Skip to main content

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 props baseUrl dan juga field path pada props spec dari komponen TableLowcode.
  • Fungsi penghapusan artikel juga akan ditangani oleh tabel artikel ini.
  • Pelajari header_action dan field_action pada props spec dari komponen TableLowcode. Di situ terdapat path yang akan diakses ketika tombol di klik. path ini 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 useArtikelList di src/api/artikel.ts. Contoh kode berikut juga sekalian membuat tipe ArtikelPayload yang 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 useArtikelDetail di src/api/artikel.ts. Catatan: Kode berikut ditambahkan, bukan mengganti kode yang sudah ada pada berkas src/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 useArtikelCreate di src/api/artikel.ts Catatan: Kode berikut ditambahkan, bukan mengganti kode yang sudah ada pada berkas src/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 useArtikelEdit di src/api/artikel.ts Catatan: Kode berikut ditambahkan, bukan mengganti kode yang sudah ada pada berkas src/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 };
}
  • 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",
},
];