@ecomcx/ecomweb-theme-sdk 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -32,6 +32,37 @@ export declare const UNLOCK_HEADER = "X-CMS-Unlock";
32
32
  /** unlock: giá trị cookie cms-unlock — có thì không dùng cache (nội dung riêng của người đã nhập mật khẩu). */
33
33
  export declare const resolvePath: (path: string, query?: Record<string, string | string[] | undefined>, noStore?: boolean, unlock?: string) => Promise<ResolveResult>;
34
34
  export declare const getPreview: (token: string) => Promise<ResolveResult>;
35
+ /** Một điều kiện lọc theo taxonomy: bài phải khớp (hoặc không khớp) các term này. */
36
+ export interface TaxQueryClause {
37
+ taxonomy: string;
38
+ terms: string[];
39
+ /** Mặc định "IN". "NOT IN" = loại bài thuộc các term này. */
40
+ operator?: "IN" | "NOT IN";
41
+ }
42
+ /**
43
+ * Một điều kiện lọc theo trường tuỳ biến (post.meta). Giá trị so sánh phải đúng kiểu JSON (số là số).
44
+ *
45
+ * So bằng (`=`, `!=`, `IN`, `NOT IN`, `EXISTS`, `NOT EXISTS`) chạy trên chỉ mục GIN sẵn có — dùng được
46
+ * với mọi khoá meta.
47
+ *
48
+ * So khoảng (`>`, `>=`, `<`, `<=`, `BETWEEN`, `LIKE`) **chỉ dùng được với khoá đã khai báo
49
+ * `filterable`** (backend tạo chỉ mục B-tree cho khoá đó lúc khởi động); khoá chưa khai báo trả 422.
50
+ * Với khoá kiểu số, giá trị phải là số JSON (chuỗi số cũng được, core tự chuyển) — nếu không phép so
51
+ * sẽ so chuỗi ("100" < "20").
52
+ */
53
+ export interface MetaQueryClause {
54
+ key: string;
55
+ compare: "=" | "!=" | "IN" | "NOT IN" | "EXISTS" | "NOT EXISTS" | ">" | ">=" | "<" | "<=" | "BETWEEN" | "LIKE";
56
+ /** Bỏ trống với EXISTS / NOT EXISTS. Với LIKE: mẫu có `%` / `_`. */
57
+ value?: string | number | boolean | (string | number | boolean)[];
58
+ /** Cận trên của BETWEEN (bắt buộc với BETWEEN). */
59
+ value2?: string | number;
60
+ }
61
+ export interface MetaQuery {
62
+ /** Mặc định "AND". */
63
+ relation?: "AND" | "OR";
64
+ clauses: MetaQueryClause[];
65
+ }
35
66
  export interface PostQuery {
36
67
  /** Mặc định: ngôn ngữ của request hiện tại. */
37
68
  locale?: string;
@@ -40,6 +71,14 @@ export interface PostQuery {
40
71
  term?: string;
41
72
  authorId?: number;
42
73
  search?: string;
74
+ /** Lọc nhiều post type cùng lúc (bài viết + trang + job...). Gộp với `type`. */
75
+ types?: string[];
76
+ /** Lọc nhiều taxonomy cùng lúc — thay cho taxonomy/term khi cần AND/OR nhiều điều kiện. */
77
+ taxQuery?: TaxQueryClause[];
78
+ /** Lọc theo trường tuỳ biến; chạy trong cơ sở dữ liệu trên chỉ mục GIN của posts.meta. */
79
+ metaQuery?: MetaQuery;
80
+ /** Chỉ lấy bài thuộc ngữ cảnh này (plugin tự lọc thêm qua filter posts.query). */
81
+ context?: string;
43
82
  include?: number[];
44
83
  exclude?: number[];
45
84
  parentId?: number;
@@ -51,6 +90,11 @@ export interface PostQuery {
51
90
  perPage?: number;
52
91
  orderBy?: "date" | "title" | "modified" | "menu_order" | "comment_count";
53
92
  order?: "asc" | "desc";
93
+ /**
94
+ * Sắp xếp theo một khoá meta đã khai báo `filterable` (vd `"gia"`, `"job_deadline"`) — thay cho `orderBy`.
95
+ * Khoá chưa khai báo trả 422 (không có chỉ mục thì sắp xếp phải quét toàn bảng).
96
+ */
97
+ orderByMeta?: string;
54
98
  }
55
99
  export declare const getPosts: ({ withContent, ...q }?: PostQuery) => Promise<{
56
100
  items: Post[];
package/dist/api.js CHANGED
@@ -116,7 +116,19 @@ export const UNLOCK_HEADER = "X-CMS-Unlock";
116
116
  /** unlock: giá trị cookie cms-unlock — có thì không dùng cache (nội dung riêng của người đã nhập mật khẩu). */
117
117
  export const resolvePath = (path, query = {}, noStore = false, unlock) => cmsFetch("/resolve", { tags: ["posts"], noStore: noStore || !!unlock, query: clean({ ...query, path }), headers: unlock ? { [UNLOCK_HEADER]: unlock } : undefined });
118
118
  export const getPreview = (token) => cmsFetch("/preview", { noStore: true, query: { token } });
119
- export const getPosts = async ({ withContent, ...q } = {}) => cmsFetch("/posts", { tags: ["posts"], query: clean({ ...q, content: withContent ?? false, locale: q.locale ?? (await getRequestLocale()) }) });
119
+ export const getPosts = async ({ withContent, ...q } = {}) => {
120
+ // Bóc tách TRONG thân hàm, không phải trong tham số: chữ ký đã công bố của getPosts nằm trong baseline
121
+ // hợp đồng (api/theme-sdk.txt) nên thêm tên vào tham số là đổi một dòng đã ký (CI coi là phá hợp đồng).
122
+ const { taxQuery, metaQuery, ...rest } = q;
123
+ return cmsFetch("/posts", { tags: ["posts"], query: clean({
124
+ ...rest,
125
+ // Bộ lọc phức tạp đi dạng JSON — core phân tích và áp thẳng vào câu SQL (không lọc bằng bộ nhớ).
126
+ taxQuery: taxQuery?.length ? JSON.stringify(taxQuery) : undefined,
127
+ metaQuery: metaQuery?.clauses?.length ? JSON.stringify(metaQuery) : undefined,
128
+ content: withContent ?? false,
129
+ locale: rest.locale ?? (await getRequestLocale()),
130
+ }) });
131
+ };
120
132
  const loc = async (locale) => locale ?? (await getRequestLocale());
121
133
  export const getPost = async (type, slug, locale) => cmsFetch(`/posts/${type}/${encodeURIComponent(slug)}`, { tags: ["posts"], query: clean({ locale: await loc(locale) }) }).catch(() => null);
122
134
  export const getTerms = async (taxonomy, hideEmpty = true, locale) => cmsFetch(`/terms/${taxonomy}`, { tags: ["terms"], query: clean({ hideEmpty, locale: await loc(locale) }) });
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { getArchives, getComments, getMenu, getMenuById, getPosts, getSite, getTerms, getWidgets } from "./api.js";
3
3
  import { findWidget, getSlotItems } from "./runtime.js";
4
- import { cx, formatDate, pageLink } from "./utils.js";
4
+ import { cx, formatDate, pageLink, pageLinkWithQuery } from "./utils.js";
5
5
  import { Img } from "./image.js";
6
6
  import { CommentForm, CommentList, CommentsMore, ExtensionBoundary, LanguageSwitcherClient, Link, SearchFormClient } from "./client.js";
7
7
  import { getT } from "./i18n.js";
@@ -102,7 +102,11 @@ export async function Pagination({ data, prevLabel, nextLabel }) {
102
102
  else if (pages[pages.length - 1] !== "…")
103
103
  pages.push("…");
104
104
  }
105
- const link = (p) => pageLink(data.basePath, p, data.kind === "search" ? data.search : null);
105
+ // Danh sách có bộ lọc (?phong-ban=…, ?muc-luong=…) → giữ nguyên bộ lọc khi chuyển trang
106
+ const hasFilters = Object.keys(data.query ?? {}).some((k) => k !== "s" && k !== "page");
107
+ const link = (p) => hasFilters
108
+ ? pageLinkWithQuery(data.basePath, p, data.query)
109
+ : pageLink(data.basePath, p, data.kind === "search" ? data.search : null);
106
110
  return (_jsxs("nav", { className: "pagination", "aria-label": t("pagination.label"), children: [data.page > 1 && _jsx(Link, { className: "prev", href: link(data.page - 1), children: prevLabel }), pages.map((p, i) => p === "…" ? _jsx("span", { className: "dots", children: "\u2026" }, `e${i}`)
107
111
  : p === data.page ? _jsx("span", { className: "current", "aria-current": "page", children: p }, p)
108
112
  : _jsx(Link, { href: link(p), children: p }, p)), data.page < data.totalPages && _jsx(Link, { className: "next", href: link(data.page + 1), children: nextLabel })] }));
package/dist/types.d.ts CHANGED
@@ -214,6 +214,12 @@ export interface ResolveResult {
214
214
  } | null;
215
215
  /** Không cho máy tìm kiếm index. */
216
216
  noIndex?: boolean;
217
+ /**
218
+ * Tham số truy vấn của URL (searchParams) của request hiện tại — server render đọc trực tiếp để
219
+ * lọc danh sách ngay trên máy chủ. Dùng cho bộ lọc người đọc chọn: `getPosts({ metaQuery: … })`
220
+ * rồi truyền `data.query` vào để giữ bộ lọc khi chuyển trang (xem `pageLinkWithQuery`).
221
+ */
222
+ query?: Record<string, string>;
217
223
  }
218
224
  /** Props mà mọi template của theme nhận được. */
219
225
  export interface TemplateProps {
package/dist/utils.d.ts CHANGED
@@ -5,4 +5,11 @@ export declare function formatDate(value: string | null | undefined, site?: Pick
5
5
  }): string;
6
6
  export declare function setting<T = string>(settings: Record<string, unknown>, key: string, fallback: T): T;
7
7
  export declare function pageLink(basePath: string, page: number, search?: string | null): string;
8
+ /**
9
+ * Link của một trang trong danh sách CÓ bộ lọc: giữ nguyên mọi tham số truy vấn hiện có (phòng ban,
10
+ * mức lương, từ khoá…) và chỉ đổi số trang. Truyền `data.query` của trang vào.
11
+ *
12
+ * <Link href={pageLinkWithQuery(data.basePath, 2, data.query)}>2</Link>
13
+ */
14
+ export declare function pageLinkWithQuery(basePath: string, page: number, query?: Record<string, string> | null): string;
8
15
  export declare function cx(...classes: Array<string | false | null | undefined>): string;
package/dist/utils.js CHANGED
@@ -24,6 +24,25 @@ export function pageLink(basePath, page, search) {
24
24
  const path = page <= 1 ? base || "/" : `${base}/page/${page}`;
25
25
  return search ? `${path}${path.includes("?") ? "&" : "?"}s=${encodeURIComponent(search)}` : path;
26
26
  }
27
+ /**
28
+ * Link của một trang trong danh sách CÓ bộ lọc: giữ nguyên mọi tham số truy vấn hiện có (phòng ban,
29
+ * mức lương, từ khoá…) và chỉ đổi số trang. Truyền `data.query` của trang vào.
30
+ *
31
+ * <Link href={pageLinkWithQuery(data.basePath, 2, data.query)}>2</Link>
32
+ */
33
+ export function pageLinkWithQuery(basePath, page, query) {
34
+ const base = basePath === "/" ? "" : basePath.replace(/\/$/, "");
35
+ const path = page <= 1 ? base || "/" : `${base}/page/${page}`;
36
+ const params = new URLSearchParams();
37
+ for (const [k, v] of Object.entries(query ?? {})) {
38
+ // "page" do đường dẫn quyết định; tham số rỗng không cần giữ
39
+ if (k === "page" || v === "" || v === undefined || v === null)
40
+ continue;
41
+ params.set(k, v);
42
+ }
43
+ const qs = params.toString();
44
+ return qs ? `${path}${path.includes("?") ? "&" : "?"}${qs}` : path;
45
+ }
27
46
  export function cx(...classes) {
28
47
  return classes.filter(Boolean).join(" ");
29
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomcx/ecomweb-theme-sdk",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "description": "SDK để viết theme và phần frontend của plugin cho Ecomweb",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",