前置き
以前から、こちらの記事のとおりPleasaterのAPIを使いやすくするために、TypeScriptで開発を進めています。今回はその続きです。
以前までは、Getしか処理がなかったので、今回はCreateを実装させてみたいと思います。
環境
主な環境は以下のとおり
- next: 14.1.0
- react: 18
- tailwindcss: 3.3.0
前回構築したNext.jsの環境をアップデートしていきます。
作るもののイメージ
前回作った画面に加え、記事投稿用の画面を追加します。
ソース
以下のような構成になっています。
app
├api //Pleansater接続のため、api routeで制御します。
│└pleasanter
│ ├getarticles
│ │└route.ts //記事取得用api
│ └postarticle
│ └route.ts //記事投稿用api
├components
│└PleasanterItem.tsx //pleasnaterから取得した情報を描画するコンポーネント
├lib
│└pleasanterClient.ts //pleasnater接続用モジュール群(今回のメイン)
├post
│└page.tsx //投稿用ページのpage.tsx
└page.tsx //メイン画面のpage.tsx
詳しくはgitを参照してください。
前回、かなり雑な状態だったので、多少整理しました。が、webアプリとしては全然機能しません。あくまでpleasnater接続の動作を確認するためと割り切ってます。
Pleasanterの使用
CreateのAPI使用はマニュアルを参照してください。
解説
主要なポイントを掻い摘んで説明していきます。
PleasanterClient.ts
本記事の主たる要素です。前回から追加したcreate要素をピックアップ
ItemModel
export class ItemModel extends Api implements RequestOpsions {
SiteId?: number | null;
UpdatedTime?: Date | null;
IssueId?: Number | null;
ResutId?: Number | null;
Ver?: Number | null;
Title?: string | null;
Body?: string | null;
StartTime?: Date | null;
CompletionTime?: Date | null;
WorkValue?: Number | null;
ProgressRate?: Number | null;
Status?: Number | null;
Manager?: Number | null;
Owner?: Number | null;
Locked?: boolean | null;
Comments?: string | null;
Creator?: Number | null;
Updator?: Number | null;
CreatedTime?: Date | null;
ProcessId?: Number | null;
ClassHash: Map<string, string> | null;
DateHash: Map<string, Date> | null;
NumHash: Map<string, Number> | null;
CheckHash: Map<string, boolean> | null;
DescriptionHash: Map<string, string> | null;
AttachmentsHash: Map<string, any> | null;
constructor(requestOpsions: RequestOpsions) {
super({});
this.SiteId = requestOpsions.SiteId || null;
this.UpdatedTime = requestOpsions.UpdatedTime || null;
this.IssueId = requestOpsions.IssueId || null;
this.ResutId = requestOpsions.ResutId || null;
this.Ver = requestOpsions.Ver || null;
this.Title = requestOpsions.Title || null;
this.Body = requestOpsions.Body || null;
this.StartTime = requestOpsions.StartTime || null;
this.CompletionTime = requestOpsions.CompletionTime || null;
this.WorkValue = requestOpsions.WorkValue || null;
this.ProgressRate = requestOpsions.ProgressRate || null;
this.Status = requestOpsions.Status || null;
this.Manager = requestOpsions.Manager || null;
this.Owner = requestOpsions.Owner || null;
this.Locked = requestOpsions.Locked || null;
this.Comments = requestOpsions.Comments || null;
this.Creator = requestOpsions.Creator || null;
this.Updator = requestOpsions.Updator || null;
this.CreatedTime = requestOpsions.CreatedTime || null;
this.ProcessId = requestOpsions.ProcessId || null;
this.ClassHash = requestOpsions.ClassHash || null;
this.DateHash = requestOpsions.DateHash || null;
this.NumHash = requestOpsions.NumHash || null;
this.CheckHash = requestOpsions.CheckHash || null;
this.DescriptionHash = requestOpsions.DescriptionHash || null;
this.AttachmentsHash = requestOpsions.AttachmentsHash || null;
}
setClassHash({ key, value }: { key: string; value: string }) {
if (this.ClassHash === null) {
this.ClassHash = new Map();
}
this.ClassHash.set(key, value);
}
setDateHash({ key, value }: { key: string; value: Date }) {
if (this.DateHash === null) {
this.DateHash = new Map();
}
this.DateHash.set(key, value);
}
setNumHash({ key, value }: { key: string; value: Number }) {
if (this.NumHash === null) {
this.NumHash = new Map();
}
this.NumHash.set(key, value);
}
setDescriptionHash({ key, value }: { key: string; value: string }) {
if (this.DescriptionHash === null) {
this.DescriptionHash = new Map();
}
this.DescriptionHash.set(key, value);
}
setAttachmentsHash({
key,
value,
}: {
key: string;
value: AttachmentsHashProps;
}) {
if (this.AttachmentsHash === null) {
this.AttachmentsHash = new Map();
}
this.AttachmentsHash.set(key, value);
}
}
createリクエストの肝となる、itemモデルです。基本的にpleasanter本体の仕様を踏襲しています。
setItemModel
static setItemModel({ item }: { item: ItemModel }) {
let data: any = {};
data.ApiKey = item.ApiKey;
data.ApiVersion = item.ApiVersion;
if (item.SiteId != null) {
data.SiteId = item.SiteId;
}
if (item.UpdatedTime != null) {
data.UpdatedTime = item.UpdatedTime;
}
if (item.IssueId != null) {
data.IssueId = item.IssueId;
}
if (item.ResutId != null) {
data.ResutId = item.ResutId;
}
if (item.Ver != null) {
data.Ver = item.Ver;
}
if (item.Title != null) {
data.Title = item.Title;
}
if (item.Body != null) {
data.Body = item.Body;
}
if (item.StartTime != null) {
data.StartTime = item.StartTime;
}
if (item.CompletionTime != null) {
data.CompletionTime = item.CompletionTime;
}
if (item.WorkValue != null) {
data.WorkValue = item.WorkValue;
}
if (item.ProgressRate != null) {
data.ProgressRate = item.ProgressRate;
}
if (item.Status != null) {
data.Status = item.Status;
}
if (item.Manager != null) {
data.Manager = item.Manager;
}
if (item.Owner != null) {
data.Owner = item.Owner;
}
if (item.Locked != null) {
data.Locked = item.Locked;
}
if (item.Comments != null) {
data.Comments = item.Comments;
}
if (item.Creator != null) {
data.Creator = item.Creator;
}
if (item.Updator != null) {
data.Updator = item.Updator;
}
if (item.CreatedTime != null) {
data.CreatedTime = item.CreatedTime;
}
if (item.ProcessId != null) {
data.ProcessId = item.ProcessId;
}
if (item.ClassHash) {
let hash: any = {};
item["ClassHash"].forEach((value, key) => {
hash[key] = value;
});
data["ClassHash"] = hash;
}
if (item.DateHash) {
let hash: any = {};
item["DateHash"].forEach((value, key) => {
hash[key] = value;
});
data["DateHash"] = hash;
}
if (item.NumHash) {
let hash: any = {};
item["NumHash"].forEach((value, key) => {
hash[key] = value;
});
data["NumHash"] = hash;
}
if (item.CheckHash) {
let hash: any = {};
item["CheckHash"].forEach((value, key) => {
hash[key] = value;
});
data["CheckHash"] = hash;
}
if (item.DescriptionHash) {
let hash: any = {};
item["DescriptionHash"].forEach((value, key) => {
hash[key] = value;
});
data["DescriptionHash"] = hash;
}
if (item.AttachmentsHash) {
let hash: any = {};
item["AttachmentsHash"].forEach((value, key) => {
hash[key] = value;
});
data["AttachmentsHash"] = hash;
}
return data;
}
createリクエストを生成するメソッドです。「ClassHash」だとかの面倒な部分は、項目を渡せばいい感じに処理してくれるようにしています。
apiCreate
static async apiCreate({
id,
item,
url,
}: {
id: number;
item: ItemModel;
url: string;
}) {
let data = PleasanterApiClient.setItemModel({ item: item });
let response = await fetch(
`${url}${url.slice(-1) !== "/" ? "/" : ""}api/items/${id}/create`,
{
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
}
);
return response.json();
}
create本体です。itemModelを引数で受け取り、fetchでpleasanterに接続、responseを返却しています。
post/page.tsx
"use client";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { useRouter } from "next/navigation";
const page = () => {
const router = useRouter();
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [classA, setClassA] = useState("");
const [classB, setClassB] = useState("");
const handleSubmit = async (e: any) => {
e.preventDefault();
const response = await fetch(
"http://localhost:3000/api/pleasanter/postarticle",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: title,
body: body,
classA: classA,
classB: classB,
}),
}
);
router.push("/");
};
return (
<div className="my-10">
<div className="ml-[250px]">
<form onSubmit={handleSubmit}>
<div className=" flex flex-col gap-4 w-[200px] items-center">
<div className="flex items-center gap-3">
<p>Title</p>
<input
className="border h-10"
type="text"
name="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div className="flex items-center gap-3">
<p>Body</p>
<input
className="border h-10"
type="text"
name="Body"
value={body}
onChange={(e) => setBody(e.target.value)}
/>
</div>
<div className="flex items-center gap-3">
<p>ClassA</p>
<input
className="border h-10"
type="text"
name="classA"
value={classA}
onChange={(e) => setClassA(e.target.value)}
/>
</div>
<div className="flex items-center gap-3">
<p>ClassB</p>
<input
className="border h-10"
type="text"
name="classB"
onChange={(e) => setClassB(e.target.value)}
/>
</div>
<Button
className=" w-[150px]"
variant="outline"
type="submit"
>
送信
</Button>
</div>
</form>
</div>
<div className="m-5">
<Button variant="outline">
<Link href="/">戻る</Link>
</Button>
</div>
</div>
);
};
export default page;
記事を投稿する画面です。よくあるリファレンスそのまんまに近い、簡単なものです。ので解説は不要かと・・・
その他は本筋ではないため、割愛。GITを参照してください。
おわりに
挙動も特段特筆すべきものはありませんので、省略。改めてgitリンクおいておきますので、気になる人は覗いてみてください。引き続きpleasnaterClient、機能拡張していきたいと思います。