React を使って、kintone にガントチャートとカンバンを表示しよう!

目次

はじめに

タスクやプロジェクトの管理で kintone を利用している方も多いと思います。
ガントチャートとカンバンの機能で kintone のデータを可視化できたら、より効率的にタスクを管理できます。
今回の記事は React を使って、ガントチャートとカンバンを kintone に表示するカスタマイズを紹介します。

デモ環境

デモ環境で実際に動作を確認できます。
https://dev-demo.cybozu.com/k/339/ (External link)

ログイン情報は cybozu developer network デモ環境 で確認してください。

完成イメージ

今回のカスタマイズを適用することで、kintone に登録したタスクのデータを、ガントチャートとカンバンの形式で表示させることができます。

できること

タスクをガントチャート形式で表示

レコード一覧画面では、すべてのタスクを表示します。

レコード詳細画面では、関連する親子タスクを表示します。

ガントチャートで表示したときの機能の詳細は、次のとおりです。

  • タスクの「タイトル」「開始日付」「完了日付」などの情報をチャートに表示します。
  • タスクの「タイプ」によって、異なる色で各タスクのチャートを表示します。
  • 「親タスク ID」フィールドの登録で、タスクの親子関係を設定できます。親子関係はガントチャートの矢印で表示されます。
  • タスクのチャートをドラッグすることで、タスクの「開始日付」「完了日付」を変更できます。
  • タスクのチャートをダブルクリックした場合、該当タスクのレコード詳細画面に遷移します。

タスクをカンバン形式で表示

レコード一覧画面では、すべてのタスクを表示します。

カンバンで表示したときの機能の詳細は、次のとおりです。

  • タスクの「タイトル」「担当者」「開始日付」「完了日付」「タイプ」などの情報をカンバンのカードに表示します。
  • 「ステータス」ごとに列を分けて、タスクを表示します。
  • タスクのカードをドラッグすることで、タスクの「ステータス」(列)を変更できます。
  • タスクの「タイプ」によって、異なる色で各タスクのラベルを表示します。
  • タスクのカードをクリックした場合、該当タスクのレコード詳細画面に遷移します。

アプリの準備

下準備として、まずはガントチャートとカンバンを表示するための「タスク管理」アプリを作成しましょう。
「はじめから作成」でアプリを新規作成し、次のすべてのフィールドをアプリに追加してください。

フィールドの種類 フィールド名 フィールドコード 備考
ドロップダウン Type type タスクのタイプ
ドロップダウン Priority priority タスクの優先度
ドロップダウン Status status タスクのステータス(カンバンの列)
日付 Start startDate タスクの開始時間
日付 End endDate タスクの完了時間
ユーザー選択 Assignee assignee タスクの担当者
文字列(1行) Summary summary タスクのタイトル
文字列 (複数行) Detail detail 詳細情報
数値 Parent Task parent 親タスク ID
関連レコード一覧 Subtasks subtasks 同じ親タスクのレコードを表示します。
  • 参照するアプリ:このアプリ
  • 表示するレコードの条件:Parent Task=Parent Task
  • 表示するフィールド:レコード番号、Summary、Assignee
スペース addSub 子タスク 追加ボタン

サンプルコード

サンプルコードは、GitHub に公開されています。
SAMPLE-ganttchart-kanban (External link)

サンプルコードのリポジトリを clone し、「SAMPLE-ganttchart-kanban」フォルダーで次のコマンドを実行してください。

1
2
npm install
npm run build

コマンドを実行したら、「dist」フォルダーの中にファイルが生成されます。

  • 「js」以下
    • app.js
    • commons.js
  • 「css」以下
    • app.css
    • commons.css

今回のカスタマイズの適用には、この 4 つのファイルを利用します。

information

「SAMPLE-ganttchart-kanban」の「src」フォルダー内に、ビルド前のファイルがあります。
ソースコードに変更を加えたい方は、そちらのファイルを編集し、ビルドしたうえで環境に適用してください。
ビルド作業は「 webpack入門 ~Babel,Polyfillを使って快適ES6ライフ~」記事を参照してください。

カスタマイズの適用

「タスク管理」アプリの設定画面を開き、[設定]タブの[JavaScript / CSS でカスタマイズ]をクリックします。
以下の画像を参考に、先ほど生成された JavaScript、CSS ファイルを「PC 用の JavaScript ファイル」、「PC 用の CSS ファイル」からアップロードします。

設定を保存したら、アプリを更新します。

動作確認

次のサンプルレコードのように、ガントチャートとカンバンに表示させたいデータを「タスク管理」アプリに登録します。

レコードを登録したら、レコードの一覧画面と詳細画面を表示します。
完成イメージ のようにガントチャート、カンバンが表示されれば成功です!

サンプルコードの解説

サンプルコードの解説をしていきます。
今回のカスタマイズは、kintone アプリにガントチャートとカンバンを表示する処理に加え、該当アプリのデータの取得/更新、表示を切り替えるボタンの生成などの処理があります。

今回のプログラムは React と TypeScript を利用しています。
React と TypeScript の使い方について確認したい方は、次の記事を合わせて確認してください。

アプリのデータを取得/更新する処理、ボタンを生成する処理

KintoneAppRepository.tsx

実際に kintone からレコードを取得、更新する処理を定義しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/*
 * react sample program
 * Copyright (c) 2022 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

import {KintoneRestAPIClient, KintoneRecordField, KintoneFormFieldProperty} from '@kintone/rest-api-client';
import stc from 'string-to-color';

// タスク管理アプリの型を定義する
export type AppRecord = {
  $id: KintoneRecordField.ID
  parent: KintoneRecordField.Number
  summary: KintoneRecordField.SingleLineText
  detail: KintoneRecordField.MultiLineText
  assignee: KintoneRecordField.UserSelect
  startDate: KintoneRecordField.Date
  endDate: KintoneRecordField.Date
  type: KintoneRecordField.Dropdown
  status: KintoneRecordField.Dropdown
}

export type AppProperty = {
  type: KintoneFormFieldProperty.Dropdown
  status: KintoneFormFieldProperty.Dropdown
}

// レコードを取得する処理
const getRecordsByApi = (query?: string) => {
  return new KintoneRestAPIClient().record.getRecords<AppRecord>({
    app: kintone.app.getId()!,
    query: `${query ? query : ''} order by $id asc`,
  });
};

// フォームの設定情報を取得する処理
const getFieldsByApi = () => {
  return new KintoneRestAPIClient().app.getFormFields<AppProperty>({app: kintone.app.getId()!});
};

// 「ステータス」フィールドの情報を更新する処理
export const updateStatus = async (recordID: string, status: string) => {
  await new KintoneRestAPIClient().record.updateRecord({
    app: kintone.app.getId()!,
    id: recordID,
    record: {
      status: {
        value: status,
      },
    },
  });
};

// 「開始日付」「完了日付」フィールドの情報を更新する処理
export const updateDate = async (recordID: string, start: string, end: string) => {
  await new KintoneRestAPIClient().record.updateRecord({
    app: kintone.app.getId()!,
    id: recordID,
    record: {
      startDate: {
        value: start,
      },
      endDate: {
        value: end,
      },
    },
  });
};

// レコード、フォームの設定情報の取得を実行する処理
export const getRecords = async (
  cb: (records: AppRecord[], status: Map<string, number>, type: Map<string, string>) => void,
  query?: string,
) => {
  const [fields, list] = await kintone.Promise.all([getFieldsByApi(), getRecordsByApi(query)]);
  const type = new Map();
  const status = new Map();
  // タスクのタイプの情報を type オブジェクトにセットする
  Object.keys(fields.properties.type.options).forEach((k) => {
    type.set(k, stc(k));
  });
  // タスクのステータスの情報を status オブジェクトに順番にセットする
  Object.keys(fields.properties.status.options).forEach((k) => {
    status.set(k, fields.properties.status.options[k].index);
  });
  cb(list.records, status, type);
};
App.tsx

ガントチャート、カンバンの表示を切り替えるボタンを生成する処理です。
デフォルトでは、カンバンの画面を表示する設定になっています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, {useEffect} from 'react';
import ReactDOM from 'react-dom';
import {Radio} from 'antd';
import GanttCharts from './GanttCharts';
import 'antd/dist/antd.css';
import Kanban from './Kanban';

// eslint-disable-next-line no-shadow
export enum AppType {
  Gantt = 'Gantt',
  Board = 'Kanban',
}

// ガントチャート、カンバンを切り替えるボタンのコンポーネントを定義する
export const AppSwitcher = () => {
  // ボタンを押したときのハンドラ
  const onAppChange = (app: AppType) => {
    switch (app) {
      case AppType.Gantt:
        ReactDOM.render(
          // ガントチャートが選択された場合、レコード一覧のメニューの下側の空白部分の要素に、ガントチャートを表示する
          <GanttCharts query={kintone.app.getQueryCondition() || undefined} />,
          kintone.app.getHeaderSpaceElement(),
        );
        break;
      default:
        // デフォルトの表示として、レコード一覧のメニューの下側の空白部分の要素に、カンバンを表示する
        ReactDOM.render(<Kanban />, kintone.app.getHeaderSpaceElement());
    }
  };

  // 初回描画時に、カンバンの描画を実行する
  useEffect(() => {
    ReactDOM.render(<Kanban />, kintone.app.getHeaderSpaceElement());
  }, []);

  // 要素の定義と返却(React UI library の Ant Design の radio components を利用)
  return (
    <>
      <Radio.Group
        defaultValue={AppType.Board}
        buttonStyle="solid"
        size="large"
        onChange={(e) => onAppChange(e.target.value)}
      >
        <Radio.Button value={AppType.Gantt}>{AppType.Gantt}</Radio.Button>
        <Radio.Button value={AppType.Board}>{AppType.Board}</Radio.Button>
      </Radio.Group>
    </>
  );
};
AddSub.tsx

子タスクのレコードを追加するボタンを生成する処理です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import {Button, Tooltip} from 'antd';
import {PlusCircleTwoTone} from '@ant-design/icons';

// 子タスクを追加するボタンのコンポーネントを定義する
const AddSub = ({id = ''}) => {
  // ボタンを押したときのハンドラ
  const onClick = () => {
    if (id) {
      const url = window.location.protocol + '//' + window.location.host + window.location.pathname + 'edit?pid=' + id;
      window.location.assign(url.replaceAll('showedit', 'edit'));
    }
  };
  // 要素の定義と返却
  return (
    <Tooltip title="add sub task">
      <Button type="primary" shape="circle" icon={<PlusCircleTwoTone />} size="large" onClick={onClick} />
    </Tooltip>
  );
};

export default AddSub;
index.tsx

画面にボタンやコンポーネントを表示します。

  • レコード一覧画面
    • ガントチャートとカンバンを切り替えるボタン
  • レコード詳細画面
    • 開いたレコードに関連する、親子タスクのガントチャート
    • 子タスクを追加するボタン
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
 * react sample program
 * Copyright (c) 2022 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

import React from 'react';
import ReactDOM from 'react-dom';
import {AppSwitcher} from './components/App';
import GanttCharts from './components/GanttCharts';
import AddSub from './components/AddSub';

interface KintoneEvent {
  record: kintone.types.SavedFields
}

// レコード一覧のメニューの右側の空白部分の要素に、ガントチャート、カンバンを切り替えるボタンを表示する
kintone.events.on('app.record.index.show', (event: KintoneEvent) => {
  ReactDOM.render(<AppSwitcher />, kintone.app.getHeaderMenuSpaceElement());
  return event;
});

// レコード詳細画面のメニューの上側の空白部分の要素に、該当レコードの関連する親子タスクのガントチャートを表示する
kintone.events.on('app.record.detail.show', (event: KintoneEvent) => {
  let query = `parent = ${event.record.$id.value} or $id= ${event.record.$id.value}`;
  event.record.parent.value && (query += ` or $id = ${event.record.parent.value}`);
  ReactDOM.render(<GanttCharts query={query} />, kintone.app.record.getHeaderMenuSpaceElement());
  // スペースフィールド「addSub」に、子タスクを追加するボタンを表示する
  ReactDOM.render(<AddSub id={event.record.$id.value} />, kintone.app.record.getSpaceElement('addSub'));
  return event;
});

// スペースフィールド「addSub」に設置した「add sub task」ボタンをクリックして、レコード追加画面を開いた場合、
// 親タスクのレコード id を、追加された子タスクのレコードの「親タスク ID」フィールドに指定する
kintone.events.on('app.record.create.show', (event: KintoneEvent) => {
  const queryString = window.location.search;
  const urlParams = new URLSearchParams(queryString);
  const pid = urlParams.get('pid');
  if (pid) {
    event.record.parent.value = pid;
  }
  return event;
});

ガントチャートとカンバンを生成する処理

GanttCharts.tsx

ガントチャートで表示するときのコンポーネントを定義しています。

このコンポーネントでは、次の処理ができます。

  • レコードのデータをガントチャートで表示する。
  • タスクをドラッグ&ドロップして、「開始日付」「完了日付」を更新する。
  • タスクをダブルクリックして、レコードの詳細画面に遷移する。
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
/*
 * echarts sample program
 * Copyright (c) 2022 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

import React, {useEffect} from 'react';
import 'gantt-task-react/dist/index.css';
import {Task, ViewMode, Gantt} from 'gantt-task-react';
import {ViewSwitcher} from './GanttViewSwitcher';
import {AppRecord, getRecords, updateDate} from '../KintoneAppRepository';
import {Spin, Result} from 'antd';
import './app.css';

// ガントチャートのコンポーネントを定義する
const GanttCharts = ({query = ''}) => {
  // ガントチャートの view, tasks, 画面のローディング状態の情報を保管する
  const [view, setView] = React.useState<ViewMode>(ViewMode.Day);
  const [tasks, setTasks] = React.useState<Task[]>();
  const [isLoading, setLoading] = React.useState<boolean>(true);

  // 各 ViewMode のガントチャートの column の幅を指定する
  let columnWidth = 60;
  if (view === ViewMode.Month) {
    columnWidth = 300;
  } else if (view === ViewMode.Week) {
    columnWidth = 250;
  }

  // ガントチャートから、タスクの「開始日付」「完了日付」を調整した場合、変更された日付をレコードに更新する
  const onTaskChange = (task: Task) => {
    const start = new Date(task.start.getTime() - task.start.getTimezoneOffset() * 60 * 1000)
      .toISOString()
      .split('T')[0];
    const end = new Date(task.end.getTime() - task.end.getTimezoneOffset() * 60 * 1000).toISOString().split('T')[0];
    updateDate(task.id, start, end);
  };

  // ガントチャートからタスクをダブルクリックするとき、タスクの詳細画面に遷移する
  const onDblClick = (task: Task) => {
    const url =
      window.location.protocol + '//' + window.location.host + window.location.pathname + 'show#record=' + task.id;
    window.location.assign(url.replaceAll('showshow', 'show'));
  };

  // 初回描画時に、ガントチャートに表示するためのレコードを取得する
  useEffect(() => {
    getRecords(display, query);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const display = (records: AppRecord[], status: Map<string, number>, type: Map<string, string>) => {
    // 取得したレコードが0件の場合、画面のローディングを停止する
    if (records.length === 0) {
      setLoading(false);
    } else {
      // 0件以上のレコードを取得した場合、取得したデータをガントチャート表示用に整形して返却する
      setTasks(
        records.map<Task>((record) => {
          return {
            id: record.$id.value,
            name: record.summary.value,
            start: new Date(record.startDate.value! + 'T00:00:00.000+09:00'),
            end: new Date(record.endDate.value! + 'T23:59:59.000+09:00'),
            progress: Math.ceil(((status.get(record.status.value!) || 0) * 100) / Math.max(status.size - 1, 1)),
            styles: {progressColor: type.get(record.type.value!) || '#ff9e0d', progressSelectedColor: '#ff9e0d'},
            dependencies: record.parent.value ? [record.parent.value] : [],
            // isDisabled: true,
          };
        }),
      );
    }
  };

  let content;
  // 0件以上のレコードを取得できた場合に表示する、ガントチャートの要素を定義する
  if (tasks && tasks.length > 0) {
    content = (
      <div>
        <ViewSwitcher onViewModeChange={(viewMode) => setView(viewMode)} />
        <Gantt
          tasks={tasks}
          viewMode={view}
          onDateChange={onTaskChange}
          onDoubleClick={onDblClick}
          listCellWidth="155px"
          columnWidth={columnWidth}
          todayColor="#FCFF19"
        />
      </div>
    );
    // レコードのデータを取得できるまでに表示する、ローディング画面の要素を定義する
  } else if (isLoading) {
    content = (
      <div className="center">
        <Spin size="large" tip="Loading..." />
      </div>
    );
    // レコードのデータを取得できなかった場合に表示する、「no data」画面の要素を定義する
  } else {
    content = <Result title="No data" />;
  }

  // content 要素を返却する
  return <>{content}</>;
};

export default GanttCharts;
GanttViewSwitcher.tsx

ガントチャートの ViewMode を調整するラジオボタンを生成する処理です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
 * echarts sample program
 * Copyright (c) 2022 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

import React from 'react';
import {ViewMode} from 'gantt-task-react';
import {Radio} from 'antd';
import 'antd/dist/antd.css';

type ViewSwitcherProps = {
  onViewModeChange: (viewMode: ViewMode) => void
}

// ガントチャートの ViewMode(Quarter Day, Half Day, Day, Week, Month)を調整するラジオボタンのコンポーネントを定義する
export const ViewSwitcher: React.FunctionComponent<ViewSwitcherProps> = ({onViewModeChange}) => {
  return (
    <>
      <Radio.Group defaultValue={ViewMode.Day} buttonStyle="solid" onChange={(e) => onViewModeChange(e.target.value)}>
        <Radio.Button value={ViewMode.QuarterDay}>{ViewMode.QuarterDay}</Radio.Button>
        <Radio.Button value={ViewMode.HalfDay}>{ViewMode.HalfDay}</Radio.Button>
        <Radio.Button value={ViewMode.Day}>{ViewMode.Day}</Radio.Button>
        <Radio.Button value={ViewMode.Week}>{ViewMode.Week}</Radio.Button>
        <Radio.Button value={ViewMode.Month}>{ViewMode.Month}</Radio.Button>
      </Radio.Group>
    </>
  );
};
Kanban.tsx

カンバンで表示するときのコンポーネントを定義しています。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
/*
 * echarts sample program
 * Copyright (c) 2022 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

import React, {useEffect} from 'react';
import Board from 'react-trello';
import {AppRecord, getRecords, updateStatus} from '../KintoneAppRepository';
import {AntdCard, KCard} from './Card';
import {Spin, Result} from 'antd';
import './app.css';

// カンバンのコンポーネントを定義する
const Kanban = () => {
  // カンバンに表示するレコードのデータ、画面のローディング状態の情報を保管する
  const [data, setData] = React.useState<ReactTrello.BoardData>();
  const [isLoading, setLoading] = React.useState<boolean>(true);

  const display = (records: AppRecord[], status: Map<string, number>, type: Map<string, string>) => {
    // 取得したレコードが0件の場合、画面のローディングを停止する
    if (records.length === 0) {
      setLoading(false);
    } else {
      // 0件以上のレコードを取得した場合、カンバンのカードを配置する lane の配列を生成する
      const lanes = new Array(status.size);
      // 取得したステータスごとに、lane をセットする
      status.forEach((v, k) => {
        lanes[v] = {
          id: k,
          title: k,
          cards: new Array<KCard>(),
        };
      });
      // 取得したデータをカンバンのカード表示用に整形し、該当ステータスの lane に渡す
      records.forEach((record) =>
        lanes[status.get(record.status.value!)!].cards!.push({
          id: record.$id.value,
          title: record.summary.value,
          label: record.type.value!,
          labelColor: type.get(record.type.value!),
          description: record.detail.value,
          assignee: record.assignee,
          startDate: record.startDate.value!,
          endDate: record.endDate.value!,
        }),
      );
      // 整形済みのデータを返却する
      setData({lanes});
    }
  };
  // 初回描画時の処理
  useEffect(() => {
    // カンバンに表示するためにレコードを取得し、取得したデータを処理する
    getRecords(display, kintone.app.getQueryCondition() || undefined);
  }, []);

  // カンバンのカードがクリックされた場合、該当カードのレコードの詳細画面を開く処理
  const onCardClick = (cardId: string) => {
    const url =
      window.location.protocol + '//' + window.location.host + window.location.pathname + 'show#record=' + cardId;
    window.location.assign(url);
  };

  // カードがドラッグされた場合、ドラッグの動作が完了後、レコードのステータスを更新する処理
  const handleDragEnd = (cardId: string, _sourceLandId: string, targetLaneId: string) => {
    updateStatus(cardId, targetLaneId);
  };

  let content;
  // レコードのデータを取得できた場合に表示する、カンバンの要素を定義する
  if (data) {
    content = (
      <Board
        data={data}
        draggable
        onCardClick={onCardClick}
        hideCardDeleteIcon
        handleDragEnd={handleDragEnd}
        style={{padding: '30px 20px', backgroundColor: '#5F9AF8'}}
        components={{Card: AntdCard}}
      />
    );
    // レコードのデータを取得できるまでに表示する、ローディング画面の要素を定義する
  } else if (isLoading) {
    content = (
      <div className="center">
        <Spin size="large" tip="Loading..." />
      </div>
    );
    // レコードのデータを取得できなかった場合に表示する、「no data」画面の要素を定義する
  } else {
    content = <Result title="No data" />;
  }
  // content 要素を返却する
  return <>{content}</>;
};
export default Kanban;

このコンポーネントでは、次の処理ができます。

  • カンバンのカードをドラッグ&ドロップして、ステータスを更新する。
  • カードをクリックして、レコードの詳細画面に遷移する。
Card.tsx

カンバンのカードを生成する処理と、各タスクの担当者の情報をカードに表示する処理です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
 * echarts sample program
 * Copyright (c) 2022 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

import {KintoneRecordField} from '@kintone/rest-api-client';
import {Card, Avatar, Tag} from 'antd';
import React from 'react';

const {Meta} = Card;

export interface KCard extends ReactTrello.DraggableCard {
  labelColor?: string
  assignee: KintoneRecordField.UserSelect
  startDate: string
  endDate: string
  onClick?: () => void
}

// カンバンのカードに表示する、タスク担当者のコンポーネントを定義する
const Avatars = (props: { assignee: KintoneRecordField.UserSelect }) => {
  // 要素の定義と返却
  return (
    <Avatar.Group
      maxCount={2}
      size="large"
      maxStyle={{
        color: '#f56a00',
        backgroundColor: '#fde3cf',
      }}
    >
      {props.assignee.value.map((element) => {
        return (
          <Avatar
            // src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
            style={{
              backgroundColor: '#15dad2',
            }}
            key={element.code}
          >
            {element.name}
          </Avatar>
        );
      })}
    </Avatar.Group>
  );
};

// カンバンのカードのコンポーネントを定義する
export const AntdCard = (props: KCard) => {
  // 要素の定義と返却(React UI library の Ant Design の card components を利用)
  return (
    <Card
      extra={<Tag color={props.labelColor}>{props.label}</Tag>}
      style={{width: 300}}
      title={props.title}
      onClick={props.onClick}
    >
      <Meta
        avatar={<Avatars assignee={props.assignee} />}
        title={`${props.startDate.substring(5)}~${props.endDate.substring(5)}`}
        description={props.description}
      />
    </Card>
  );
};

おわりに

今回の記事は、React を使ってガントチャートとカンバンを kintone に表示するカスタマイズを紹介しました。
統計ダッシュボードや、レーダーチャートなどを kintone に表示するカスタマイズ を紹介する記事もありますので、興味ある方はぜひ確認してみてください。

この記事で利用しているライブラリ

information

この Tips は、2022 年 6 月版 cybozu.com で動作を確認しています。