Bỏ qua để đến nội dung

Thêm sub-collection hd_* mới

  • Thư mụcadmin/packages/helpdesk/app/
    • data/helpdesk-schema.ts collections + fields + relations (sort 2→19)
    • composables/use-helpdesk-<name>.ts entity store mới
  • Thư mụcadmin/packages/helpdesk/server/api/helpdesk/<name>/
    • index.get.ts · index.post.ts · [id].get.ts · [id].patch.ts · [id].delete.ts
  • Thư mụcbackend/extensions/helpdesk/src/endpoints/
    • controllers/meta.controller.ts ALLOWED (nếu form cần field metadata)
    • controllers/crud.factory.ts makeCrud(collection) — factory dùng chung
    • controllers/<name>.controller.ts 1 dòng gọi makeCrud(‘hd_<name>’)
    • routes.ts registerCrud(router, ctx, ‘/<name>’, Controller)

Bước 1 — Schema: collection + field, đúng thứ tự sort

Phần tiêu đề “Bước 1 — Schema: collection + field, đúng thứ tự sort”

Entry đăng ký collection (không field) — ví dụ hd_labels:

app/data/helpdesk-schema.ts:25-29
{
collection: "hd_labels",
meta: { hidden: true, sort: 3, group: "helpdesk", accountability: "all" },
schema: { name: "hd_labels" },
},

Field khai báo tách rời trong cùng mảng collections (xem Thêm field vào collection để biết shape field), quan hệ (FK/M2M) khai báo trong mảng relations riêng ở cuối file.

FK ordering — sort chạy 2 → 19, phải tôn trọng phụ thuộc:

sort collection Vì sao ở vị trí này
2–7 hd_working_hours, hd_labels, hd_teams, hd_sla_policies, hd_settings, hd_connections Bảng lookup/độc lập — không phụ thuộc FK nào
8 hd_agents Phụ thuộc c_contacts (đã tồn tại từ trước — setup chạy trên DB có sẵn contacts)
9 hd_inboxes Phụ thuộc connections, agents, teams, working_hours
10–11 hd_templates, hd_canned_responses Phụ thuộc inboxes/teams
12 hd_conversations Phụ thuộc inboxes, agents, teams, sla_policies, c_contacts
13 hd_messages Phụ thuộc conversations, agents, contacts, templates, chính nó (reply-chain)
14–16 hd_conversation_labels, hd_inbox_agents, hd_team_agents Junction M2M
17 hd_message_attachments Phụ thuộc messages + odp_files
18–19 hd_automation_rules, hd_saved_views Phụ thuộc agents (owner)

Collection mới thêm vào sau mọi collection nó tham chiếu FK — sai thứ tự này setup wizard sẽ fail vì FK trỏ tới bảng chưa tồn tại.

Bước 2 — Allowlist field-metadata (nếu form cần)

Phần tiêu đề “Bước 2 — Allowlist field-metadata (nếu form cần)”

Nếu form của collection mới cần đọc metadata field qua endpoint field-registry, thêm tên collection vào ALLOWED ở backend:

backend/extensions/helpdesk/src/endpoints/controllers/meta.controller.ts
const ALLOWED = new Set<string>([
'hd_inboxes', 'hd_agents', 'hd_teams', 'hd_sla_policies', 'hd_working_hours',
'hd_automation_rules', 'hd_canned_responses', 'hd_templates', 'hd_saved_views',
'hd_connections', 'hd_settings', 'hd_conversations', 'hd_messages',
'c_contacts',
// 'hd_ten_moi', ← thêm dòng này nếu form cần field metadata
]);

Bỏ qua bước này nếu form mới viết tay hoàn toàn (không cần schema-driven hint).

Bước 3 — Controller: tái dùng crud.factory.ts

Phần tiêu đề “Bước 3 — Controller: tái dùng crud.factory.ts”

makeCrud(collection) trả sẵn 5 handler list/get/create/update/remove với pagination cap + MetaService count:

backend/extensions/helpdesk/src/endpoints/controllers/crud.factory.ts:15-17
export function makeCrud(collection: string) {
// list/get/create/update/remove — readByQuery + MetaService, clamp limit
return { list, get, create, update, remove };
}

Controller cho collection mới chỉ còn 1 dòng (mirror labels.controller.ts):

backend/extensions/helpdesk/src/endpoints/controllers/<name>.controller.ts (đầy đủ)
import { makeCrud } from './crud.factory.js';
export const { list, get, create, update, remove } = makeCrud('hd_<name>');

Bước 4 — Route: registerCrud + withAccess guard

Phần tiêu đề “Bước 4 — Route: registerCrud + withAccess guard”
backend/extensions/helpdesk/src/endpoints/routes.ts:20-44 — helper
function guard(ctx: AppContext, action: string, handler: (ctx: AppContext) => any) {
return withAccess(ctx, 'helpdesk', action, handler(ctx));
}
function registerCrud(
router: any,
ctx: AppContext,
base: string,
controller: {
list: (ctx: AppContext) => any;
get: (ctx: AppContext) => any;
create: (ctx: AppContext) => any;
update: (ctx: AppContext) => any;
remove: (ctx: AppContext) => any;
},
): void {
router.get(`${base}`, guard(ctx, 'read', controller.list));
router.post(`${base}`, guard(ctx, 'create', controller.create));
router.get(`${base}/:id(\\d+)`, guard(ctx, 'read', controller.get));
router.patch(`${base}/:id(\\d+)`, guard(ctx, 'update', controller.update));
router.delete(`${base}/:id(\\d+)`, guard(ctx, 'delete', controller.remove));
}

Đăng ký (mirror labels — routes.ts:63-64):

routes.ts
// <Name> CRUD
registerCrud(router, ctx, '/<name>', NameController);

BFF route (mirror labels/index.get.ts + [id].patch.ts):

server/api/helpdesk/<name>/index.get.ts (đầy đủ)
import { customEndpoint } from "@odp/sdk"
export default defineEventHandler(async (event) => {
assertPermission(event, { module: "helpdesk", action: "read" })
const userApi = event.context.userApi
const query = getQuery(event)
const result = await userApi.request(customEndpoint({
path: "/helpdesk/<name>/",
method: "GET",
params: query,
}))
return result
})
server/api/helpdesk/<name>/[id].patch.ts (đầy đủ)
import { customEndpoint } from "@odp/sdk"
export default defineEventHandler(async (event) => {
assertPermission(event, { module: "helpdesk", action: "update" })
const userApi = event.context.userApi
const id = getRouterParam(event, "id")!
const body = await readBody(event)
const result = await userApi.request(customEndpoint({
path: `/helpdesk/<name>/${id}`,
method: "PATCH",
body: JSON.stringify(body),
}))
return { data: result }
})

Entity store — copy nguyên khuôn use-helpdesk-labels.ts (60 dòng), đổi tên collection/type:

app/composables/use-helpdesk-<name>.ts (khuôn mẫu, rút gọn)
export function useHelpdesk<Name>() {
const api = useHelpdeskApi()
const items = useState<Name[]>("helpdesk:<name>", () => [])
const loaded = useState<boolean>("helpdesk:<name>:loaded", () => false)
async function fetchAll() {
const res = await api.list<Name>()
items.value = res.data
loaded.value = true
}
async function ensureLoaded() {
if (!loaded.value) await fetchAll()
}
function getById(id: string) {
return items.value.find(x => x.id === id)
}
async function create(input: Omit<Name, "id">) {
const created = await api.create<Name>(input as Partial<Name>)
items.value = [...items.value, created]
return created
}
async function update(id: string, patch: Partial<Name>) {
const updated = await api.update<Name>(id, patch)
const idx = items.value.findIndex(x => x.id === id)
if (idx !== -1) { const next = [...items.value]; next[idx] = updated; items.value = next }
return updated
}
async function remove(id: string) {
await api.remove<Name>(id)
items.value = items.value.filter(x => x.id !== id)
}
return { items, loaded, fetchAll, ensureLoaded, getById, create, update, remove }
}

Pattern useState key: "helpdesk:<name>" cho list, "helpdesk:<name>:loaded" cho cờ đã fetch — giữ đúng convention để tránh đụng key với store khác.

Cuối cùng thêm hàm CRUD tương ứng vào use-helpdesk-api.ts (theo khuôn Teams đã có trong Frontend), rồi wire component/page tiêu thụ store mới.