Skip to content

Commit

Permalink
Merge pull request #53 from codeforjapan/revert-29-fix/remove-tempora…
Browse files Browse the repository at this point in the history
…l-pagination

Revert "fix: remove pagination temporal logic"
  • Loading branch information
sushichan044 authored Mar 7, 2025
2 parents d98b12c + 3da3496 commit 143afa0
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 4 deletions.
11 changes: 7 additions & 4 deletions app/feature/search/components/SearchPagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Fa6SolidAngleLeft from "~icons/fa6-solid/angle-left";
import Fa6SolidAngleRight from "~icons/fa6-solid/angle-right";

import type { PaginationMeta } from "../../../generated/api/schemas/paginationMeta";
import { buildPaginationMeta } from "../pagination";
import type { noteSearchParamSchema } from "../validation";

type PaginationProps = {
Expand All @@ -28,16 +29,18 @@ export const SearchPagination = ({
visibleItemCount,
...groupProps
}: PaginationProps) => {
const pagination = buildPaginationMeta(meta, currentQuery);

const pageFirstItemIndex = currentQuery.offset + 1;
const totalDisplayedItems = currentQuery.offset + visibleItemCount;

const prevTo = useMemo(
() => (meta?.prev ? withQuery("/", getQuery(meta.prev)) : null),
[meta?.prev],
() => (pagination?.prev ? withQuery("/", getQuery(pagination.prev)) : null),
[pagination?.prev],
);
const nextTo = useMemo(
() => (meta?.next ? withQuery("/", getQuery(meta.next)) : null),
[meta?.next],
() => (pagination?.next ? withQuery("/", getQuery(pagination.next)) : null),
[pagination?.next],
);

const [clickedButton, setClickedButton] = useState<"prev" | "next">();
Expand Down
85 changes: 85 additions & 0 deletions app/feature/search/pagination.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { getQuery } from "ufo";
import { describe, expect, test } from "vitest";
import type { z } from "zod";

import type { PaginationMeta } from "../../generated/api/schemas/paginationMeta";
import { buildPaginationMeta } from "./pagination";
import type { noteSearchParamSchema } from "./validation";

describe("buildPaginationMeta", () => {
test("API 修正前ロジック: 現在のクエリパラメータから PaginationMeta を生成できる", () => {
const currentQuery = {
post_includes_text: "地震",
limit: 10,
offset: 10,
} satisfies z.infer<typeof noteSearchParamSchema>;

// API が limit, offset 以外のクエリパラメータを削除してしまう挙動を再現
const currentBrokenMeta = {
next: "https://example.com/api/v1/data/search?offset=20&limit=10",
prev: "https://example.com/api/v1/data/search?offset=0&limit=10",
} satisfies PaginationMeta;

const paginationMeta = buildPaginationMeta(currentBrokenMeta, currentQuery);
const prevQuery = paginationMeta.prev
? getQuery(paginationMeta.prev)
: null;
const nextQuery = paginationMeta.next
? getQuery(paginationMeta.next)
: null;

expect(prevQuery).toStrictEqual({
limit: "10",
offset: "0",
post_includes_text: "地震",
});
expect(nextQuery).toStrictEqual({
limit: "10",
offset: "20",
post_includes_text: "地震",
});
});

test("API 修正前ロジック: offset が 0 より大きい AND limit より小さい場合も prev を生成できる", () => {
const currentQuery = {
post_includes_text: "地震",
limit: 15,
offset: 10,
} satisfies z.infer<typeof noteSearchParamSchema>;

// API が limit, offset 以外のクエリパラメータを削除してしまう挙動を再現
const currentBrokenMeta = {
next: "https://example.com/api/v1/data/search?offset=25&limit=15",
prev: "https://example.com/api/v1/data/search?offset=0&limit=15",
} satisfies PaginationMeta;

const fixedMeta = buildPaginationMeta(currentBrokenMeta, currentQuery);
const prevQuery = fixedMeta.prev ? getQuery(fixedMeta.prev) : null;

expect(prevQuery).toStrictEqual({
limit: "15",
offset: "0",
post_includes_text: "地震",
});
});

test("API 修正前ロジック: 前のページが存在しない場合に prev が null になる", () => {
const currentQuery = {
post_includes_text: "地震",
limit: 10,
offset: 0,
} satisfies z.infer<typeof noteSearchParamSchema>;

// API が limit, offset 以外のクエリパラメータを削除してしまう挙動を再現
const currentBrokenMeta = {
next: "https://example.com/api/v1/data/search?offset=10&limit=10",
prev: null,
} satisfies PaginationMeta;

const fixedMeta = buildPaginationMeta(currentBrokenMeta, currentQuery);

const prevQuery = fixedMeta.prev ? getQuery(fixedMeta.prev) : null;

expect(prevQuery).toBe(null);
});
});
50 changes: 50 additions & 0 deletions app/feature/search/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { parseURL, stringifyParsedURL, withQuery } from "ufo";
import type { z } from "zod";

import type { PaginationMeta } from "../../generated/api/schemas/paginationMeta";
import type { noteSearchParamSchema } from "./validation";

/**
* API 側のページネーション情報が正しい情報を返すようになるまで、現在のパラメータから次のページと前のページの URL を生成する
* @param meta
* API が返したページネーション情報。前後ページが存在するかどうかの確認にのみ使用する
* @param currentQuery
* 現在のクエリパラメータ
* @returns
* 修正後のページネーション情報
*/
// API が正常な PaginationMeta を返すようになったらこの巻数で計算する必要はない
export const buildPaginationMeta = (
meta: PaginationMeta,
currentQuery: z.infer<typeof noteSearchParamSchema>,
): PaginationMeta => {
const { limit, offset, ...rest } = currentQuery;

if (meta.next == null && meta.prev == null) {
return {
next: null,
prev: null,
};
}

const nextOffset = offset + limit;

// offset が 0: 前のページが存在しない
// offset が 10, limit が 15: 前のページが存在する
const prevOffset = Math.max(offset - limit, 0);

const isFirstPage = offset === 0;

// 必ず prev か next が存在する
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const url = parseURL((meta.prev ?? meta.next)!);
url.search = "";
const baseUrl = stringifyParsedURL(url);

return {
next: withQuery(baseUrl, { ...rest, limit, offset: nextOffset }),
prev: isFirstPage
? null
: withQuery(baseUrl, { ...rest, limit, offset: prevOffset }),
};
};

0 comments on commit 143afa0

Please sign in to comment.