CRUD
Kita akan mencoba membuat CRUD sederhana untuk artikel.
Jika pada tahap-tahap berikut ada kalimat yang merujuk pada suatu berkas (misal: src/feature/ArtikelTable.tsx),
buatlah berkas tersebut pada path yang sesuai (misal: src/feature/ArtikelTable.tsx), jika belum ada.
Berikut endpoint yang akan kita pakai:
- GET
https://alurkerja-starter.alurkerja.com/crud/artikel - POST
https://alurkerja-starter.alurkerja.com/crud/artikel - GET
https://alurkerja-starter.alurkerja.com/crud/artikel/{id} - POST
https://alurkerja-starter.alurkerja.com/crud/artikel/{id} - DELETE
https://alurkerja-starter.alurkerja.com/crud/artikel/{id}
Swagger: https://alurkerja-starter.alurkerja.com/swagger-ui/index.html#/artikel-controller
Bantuan untuk formulir
Biasanya kita menginstal react-hook-form untuk mengelola formulir pada React.
npm install react-hook-form
Kita juga bisa menginstal resolver untuk react-hook-form sebagai bantuan validasi formulir.
npm install @hookform/resolvers zod --legacy-peer-deps
Tabel artikel
- Buatlah tabel artikel di
src/feature/ArtikelTable.tsx. Untuk menghubungkan dengan API, harap perhatikan propsbaseUrldan juga fieldpathpada propsspecdari komponenTableLowcode. - Fungsi penghapusan artikel juga akan ditangani oleh tabel artikel ini.
- Pelajari
header_actiondanfield_actionpada propsspecdari komponenTableLowcode. Di situ terdapatpathyang akan diakses ketika tombol di klik.pathini bisa berfungsi untuk berpindah halaman, ataupun menembak API seperti pada kasus penghapusan artikel.
import { TableLowcode } from "alurkerja-ui";
import { useState } from "react";
import { useArtikelList } from "../api/artikel";
export function ArtikelTable() {
const [pageConfig, setPageConfig] = useState({ limit: 10, page: 0 });
const [renderState, setRenderState] = useState(0);
const [search, setSearch] = useState<string | undefined>("tes");
const { data, isPending } = useArtikelList();
if (isPending) {
return <div>Loading...</div>;
}
return (
<TableLowcode
baseUrl="https://alurkerja-starter.alurkerja.com"
spec={{
show_as_menu: false,
base_url: "",
name: "",
can_bulk: false,
can_create: true,
can_delete: true,
can_edit: true,
can_detail: true,
path: "/crud/artikel",
is_bpmn: false,
is_usertask: false,
label: "",
description: "",
header_action: [
{
label: "Tambah",
action_label: "Tambah",
method: "post",
form_type: "new_page",
path: "/artikel/create",
icon: "plus",
type: "primary",
},
],
field_action: [
{
label: "Detail",
action_label: "Detail",
method: "get",
form_type: "new_page",
path: "/artikel/{id}",
icon: "eye",
type: "primary",
},
{
label: "Edit",
action_label: "Edit",
method: "put",
form_type: "new_page",
path: `/artikel/{id}/edit`,
icon: "edit",
type: "primary",
},
{
label: "Hapus",
action_label: "Hapus",
method: "delete",
form_type: "confirm_modal",
confirm: {
title: "Hapus",
message: "Apakah anda yakin ingin menghapus data ini?",
confirm_text: "Ya",
cancel_text: "Tidak",
},
path: `/crud/artikel/{id}`,
icon: "trash",
type: "danger",
},
],
fields: {},
}}
column={[
{
label: "Name",
key: "name",
},
{
label: "Deskripsi",
key: "description",
},
]}
data={data.content}
renderState={renderState}
setRenderState={setRenderState}
pageConfig={pageConfig}
setPageConfig={setPageConfig}
search={search}
setSearch={setSearch}
canFilter={false}
/>
);
}
- Hook
useArtikelListdisrc/api/artikel.ts. Contoh kode berikut juga sekalian membuat tipeArtikelPayloadyang digunakan untuk penambahan artikel.
import { useEffect, useState } from "react";
import { axiosInstance } from "../lib/axios";
import axios from "axios";
export type ArtikelDTO = {
id: string;
name: string;
description: string;
};
type ArtikelPayload = Omit<ArtikelDTO, "id">;
export type ArtikelList = {
content: ArtikelDTO[];
};
export function useArtikelList() {
const [data, setData] = useState<ArtikelList>({
content: [],
});
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState<unknown>();
useEffect(() => {
async function getArtikelList() {
setIsPending(true);
try {
const response = await axiosInstance.get("/crud/artikel");
if (axios.isAxiosError(response)) {
setError(response);
} else {
setData(response.data?.data);
}
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
getArtikelList();
}, []);
return { data, isPending, error };
}
Detail artikel
- Detail artikel di
src/feature/ArtikelDetail.tsx
import { useForm } from "react-hook-form";
import { useArtikelDetail } from "../api/artikel";
import { Link, useParams } from "react-router-dom";
import { Input } from "alurkerja-ui";
export function ArtikelDetail() {
const { id } = useParams();
const { data, isPending, error } = useArtikelDetail(id, {
enabled: Boolean(id),
});
const { register } = useForm({
values: data,
});
if (isPending) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error</div>;
}
return (
<div className="flex flex-col gap-y-4">
<div>
<label htmlFor="name">Nama</label>
<Input id="name" {...register("name")} disabled />
</div>
<div>
<label htmlFor="reason">Alasan</label>
<Input id="reason" {...register("description")} textArea disabled />
</div>
<Link to=".." className="w-fit">
Kembali
</Link>
</div>
);
}
- Hook
useArtikelDetaildisrc/api/artikel.ts. Catatan: Kode berikut ditambahkan, bukan mengganti kode yang sudah ada pada berkassrc/api/artikel.ts.
export function useArtikelDetail(
id?: string,
options: { enabled?: boolean } = {
enabled: true,
}
) {
const [data, setData] = useState<ArtikelDTO>();
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState<unknown>();
useEffect(() => {
async function getArtikelDetail() {
setIsPending(true);
try {
const response = await axiosInstance.get(`/crud/artikel/${id}`);
if (axios.isAxiosError(response)) {
setError(response);
} else {
console.log(response.data);
setData(response.data?.data);
}
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
if (options.enabled) {
getArtikelDetail();
}
}, [id, options.enabled]);
return { data, isPending, error };
}
Tambah artikel
- Form penambahan artikel di
src/feature/ArtikelCreate.tsx
import { Button, Input } from "alurkerja-ui";
import { useForm } from "react-hook-form";
import { useArtikelCreate } from "../api/artikel";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Link } from "react-router-dom";
const artikelCreateFormSchema = z.object({
name: z.string().min(1, { message: "Nama wajib diisi" }),
description: z.string().min(1, { message: "Alasan wajib diisi" }),
});
type ArtikelCreateFormSchema = z.infer<typeof artikelCreateFormSchema>;
export function ArtikelCreate() {
const { mutate, isPending, error } = useArtikelCreate();
const { register, handleSubmit, formState } =
useForm<ArtikelCreateFormSchema>({
resolver: zodResolver(artikelCreateFormSchema),
});
const nameError = formState.errors["name"];
const reasonError = formState.errors["description"];
function handleCreateArtikel(values: ArtikelCreateFormSchema) {
mutate(values);
}
return (
<form
onSubmit={handleSubmit(handleCreateArtikel)}
className="flex flex-col gap-y-4"
>
<div>
<label htmlFor="name">Nama</label>
<Input id="name" {...register("name")} />
{nameError && <p>{nameError.message}</p>}
</div>
<div>
<label htmlFor="reason">Deskripsi</label>
<Input id="reason" {...register("description")} textArea />
{reasonError && <p>{reasonError.message}</p>}
</div>
<div>
<Link to=".." className="w-fit">
Kembali
</Link>
<Button loading={isPending}>Kirim</Button>
</div>
{Boolean(error) && <p>Error</p>}
</form>
);
}
- Hook
useArtikelCreatedisrc/api/artikel.tsCatatan: Kode berikut ditambahkan, bukan mengganti kode yang sudah ada pada berkassrc/api/artikel.ts.
export function useArtikelCreate() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<unknown>();
async function mutate(values: ArtikelPayload) {
setIsPending(true);
try {
await axiosInstance.post("/crud/artikel", values);
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
return { mutate, isPending, error };
}
Edit artikel
- Form edit artikel di
src/feature/ArtikelEdit.tsx
import { useForm } from "react-hook-form";
import { useArtikelDetail, useArtikelEdit } from "../api/artikel";
import { Link, useParams } from "react-router-dom";
import { Button, Input } from "alurkerja-ui";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const artikelEditFormSchema = z.object({
name: z.string().min(1, { message: "Nama wajib diisi" }),
description: z.string().min(1, { message: "Alasan wajib diisi" }),
});
type ArtikelEditFormSchema = z.infer<typeof artikelEditFormSchema>;
export function ArtikelEdit() {
const { id } = useParams();
const {
data,
isPending: isFetchingArtikel,
error: detailError,
} = useArtikelDetail(id, {
enabled: Boolean(id),
});
const {
mutate,
isPending: isSubmitting,
error: submitError,
} = useArtikelEdit();
const { register, handleSubmit } = useForm({
resolver: zodResolver(artikelEditFormSchema),
values: data,
});
function handleEdit(values: ArtikelEditFormSchema) {
mutate(id!, values);
}
if (isFetchingArtikel) {
return <div>Loading...</div>;
}
if (detailError || submitError) {
return <div>Error</div>;
}
return (
<div className="flex flex-col gap-y-4" onClick={handleSubmit(handleEdit)}>
<div>
<label htmlFor="name">Nama</label>
<Input id="name" {...register("name")} />
</div>
<div>
<label htmlFor="reason">Alasan</label>
<Input id="reason" {...register("description")} textArea />
</div>
<div>
<Link to=".." className="w-fit">
Kembali
</Link>
<Button loading={isSubmitting}>Kirim</Button>
</div>
{Boolean(submitError) && <p>Error</p>}
</div>
);
}
- Hook
useArtikelEditdisrc/api/artikel.tsCatatan: Kode berikut ditambahkan, bukan mengganti kode yang sudah ada pada berkassrc/api/artikel.ts.
export function useArtikelEdit() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<unknown>();
async function mutate(id: string, values: ArtikelPayload) {
setIsPending(true);
try {
await axiosInstance.post("/crud/artikel/" + id, values);
} catch (error) {
console.error(error);
setError(error);
}
setIsPending(false);
}
return { mutate, isPending, error };
}
Navigasi
- Menambahkan rute untuk artikel di
src/routes.tsx
import { RouteObject } from "react-router-dom";
import { CutiTable } from "./feature/CutiTable";
import { CutiCreate } from "./feature/CutiCreate";
import { RoleGate } from "./feature/RoleGate";
import { Layout } from "./feature/Layout";
import { CutiDetail } from "./feature/CutiDetail";
import { KonfirmasiCutiTable } from "./feature/KonfirmasiCutiTable";
import { KonfirmasiCutiDetail } from "./feature/KonfirmasiCutiDetail";
import { KonfirmasiCutiEdit } from "./feature/KonfirmasiCutiEdit";
import { ArtikelTable } from "./feature/ArtikelTable";
import { ArtikelCreate } from "./feature/ArtikelCreate";
import { ArtikelDetail } from "./feature/ArtikelDetail";
import { ArtikelEdit } from "./feature/ArtikelEdit";
export const routes: RouteObject[] = [
{
path: "/",
element: <Layout />,
errorElement: <div>404</div>,
children: [
{
index: true,
element: <div>Selamat Datang</div>,
},
{
path: "cuti",
children: [
{
index: true,
element: (
<RoleGate allowed="Pegawai">
<CutiTable />
</RoleGate>
),
},
{
path: "create",
element: (
<RoleGate allowed="Pegawai">
<CutiCreate />
</RoleGate>
),
},
],
},
{
path: "artikel",
children: [
{
index: true,
element: <ArtikelTable />,
},
{
path: "create",
element: <ArtikelCreate />,
},
{
path: ":id",
element: <ArtikelDetail />,
},
{
path: ":id/edit",
element: <ArtikelEdit />,
},
],
},
],
},
];
- Menambahkan menu artikel di
src/menuConfig.ts
import { MenuConfig } from "alurkerja-ui";
export const pegawaiMenuConfig: MenuConfig[] = [
{
label: "Beranda",
href: "/",
},
{
label: "Pengajuan Cuti",
href: "/cuti",
},
{
label: "Artikel",
href: "/artikel",
},
];
export const hrdMenuConfig: MenuConfig[] = [
{
label: "Beranda",
href: "/",
},
{
label: "Konfirmasi Cuti",
href: "/konfirmasi-cuti",
},
{
label: "Artikel",
href: "/artikel",
},
];