@csntgao/uni-base 0.1.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/.prettierignore +4 -0
- package/.prettierrc.json +6 -0
- package/AGENTS.md +519 -0
- package/CLAUDE.md +512 -0
- package/README.md +734 -0
- package/docs//346/225/260/345/255/227/345/221/230/345/267/245/345/256/214/346/225/264/346/212/275/345/261/211/346/216/245/345/205/245/350/257/264/346/230/216.md +110 -0
- package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/215/225/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +79 -0
- package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/244/232/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +93 -0
- package/docs//350/264/246/346/210/267/344/270/216/347/231/273/345/275/225/347/273/204/344/273/266/345/212/250/346/200/201/346/216/245/345/205/245/350/257/264/346/230/216.md +201 -0
- package/eslint.config.js +30 -0
- package/examples/vanilla/index.html +33 -0
- package/examples/vanilla/main.js +247 -0
- package/examples/vanilla/package.json +15 -0
- package/examples/vanilla/public/runtime-config.js +10 -0
- package/examples/vanilla/style.css +99 -0
- package/package.json +31 -0
- package/packages/core/package.json +30 -0
- package/packages/core/src/client.ts +1132 -0
- package/packages/core/src/customer-support-client.ts +825 -0
- package/packages/core/src/customer-support-types.ts +123 -0
- package/packages/core/src/index.ts +79 -0
- package/packages/core/src/notification-center-client.ts +462 -0
- package/packages/core/src/notification-center-types.ts +154 -0
- package/packages/core/src/protocol.ts +237 -0
- package/packages/core/src/types.ts +170 -0
- package/packages/core/tests/client.test.ts +760 -0
- package/packages/core/tests/customer-support-client.test.ts +576 -0
- package/packages/core/tests/fake-websocket.ts +55 -0
- package/packages/core/tests/notification-center-client.test.ts +467 -0
- package/packages/core/tests/protocol.test.ts +124 -0
- package/packages/core/tsconfig.json +7 -0
- package/packages/core/tsup.config.ts +11 -0
- package/packages/web-components/package.json +35 -0
- package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
- package/packages/web-components/src/account-context.ts +1540 -0
- package/packages/web-components/src/application-switcher-transport.ts +243 -0
- package/packages/web-components/src/application-switcher.ts +1103 -0
- package/packages/web-components/src/chat.ts +1754 -0
- package/packages/web-components/src/customer-support-chat.ts +862 -0
- package/packages/web-components/src/customer-support-transport.ts +269 -0
- package/packages/web-components/src/index.ts +256 -0
- package/packages/web-components/src/notification-center.ts +1066 -0
- package/packages/web-components/src/notification-popover.ts +615 -0
- package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
- package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
- package/packages/web-components/src/organization-onboarding.ts +1216 -0
- package/packages/web-components/src/organization-selector.ts +534 -0
- package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
- package/packages/web-components/src/prototype-icons.ts +242 -0
- package/packages/web-components/src/safe-image-url.ts +29 -0
- package/packages/web-components/src/safe-markdown.ts +192 -0
- package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
- package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
- package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
- package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
- package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
- package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
- package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
- package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
- package/packages/web-components/src/user-login-transport.ts +563 -0
- package/packages/web-components/src/user-login.ts +2027 -0
- package/packages/web-components/src/user-menu.ts +880 -0
- package/packages/web-components/tests/account-context.test.ts +798 -0
- package/packages/web-components/tests/application-switcher.test.ts +562 -0
- package/packages/web-components/tests/chat.test.ts +1130 -0
- package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
- package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
- package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
- package/packages/web-components/tests/notification-center.test.ts +429 -0
- package/packages/web-components/tests/notification-popover.test.ts +283 -0
- package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
- package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
- package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
- package/packages/web-components/tests/organization-selector.test.ts +127 -0
- package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
- package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
- package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
- package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
- package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
- package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
- package/packages/web-components/tests/user-login-transport.test.ts +443 -0
- package/packages/web-components/tests/user-login.test.ts +626 -0
- package/packages/web-components/tests/user-menu.test.ts +271 -0
- package/packages/web-components/tsconfig.json +8 -0
- package/packages/web-components/tsup.config.ts +12 -0
- package/packages/web-components/tsup.runtime.config.ts +37 -0
- package/pnpm-workspace.yaml +6 -0
- package/tsconfig.base.json +21 -0
- package/vitest.config.ts +20 -0
- package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createUniplatBaseNotificationCenterTransport } from '../src/uniplat-base-notification-center-transport'
|
|
3
|
+
|
|
4
|
+
const SERVICE_BASE = 'https://gateway.example.com/api/general/project/uniplat_base/service'
|
|
5
|
+
|
|
6
|
+
interface FakeCall {
|
|
7
|
+
url: string
|
|
8
|
+
init: RequestInit
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function jsonResponse(data: unknown): Response {
|
|
12
|
+
return {
|
|
13
|
+
ok: true,
|
|
14
|
+
async json() {
|
|
15
|
+
return { rescode: 0, data }
|
|
16
|
+
},
|
|
17
|
+
async blob() {
|
|
18
|
+
return new Blob(['x'])
|
|
19
|
+
},
|
|
20
|
+
} as unknown as Response
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('createUniplatBaseNotificationCenterTransport', () => {
|
|
24
|
+
it('posts inbox with application_name in body and JWT only in the Authorization header', async () => {
|
|
25
|
+
const calls: FakeCall[] = []
|
|
26
|
+
const fetchMock = vi.fn(async (url: string, init: RequestInit) => {
|
|
27
|
+
calls.push({ url, init })
|
|
28
|
+
return jsonResponse({
|
|
29
|
+
items: [
|
|
30
|
+
{
|
|
31
|
+
notification_id: '301',
|
|
32
|
+
notification_type: 'alert',
|
|
33
|
+
notification_type_name: '预警',
|
|
34
|
+
title: '合同到期预警',
|
|
35
|
+
summary: '陈晓劳动合同 6/30 到期',
|
|
36
|
+
content: '正文',
|
|
37
|
+
content_type: 'markdown',
|
|
38
|
+
read_status: 'unread',
|
|
39
|
+
created_at: '2026-05-30T16:35:00.000Z',
|
|
40
|
+
source: { type: 'ai_employee', code: 'archive', name: '档案管理专员' },
|
|
41
|
+
resources: [{ type: 'service_order', id: 'so-1', title: '服务单' }],
|
|
42
|
+
attachments: [
|
|
43
|
+
{ file_id: 'f1', name: 'report.pdf', size_bytes: 1600, mime_type: 'application/pdf' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
has_more: true,
|
|
48
|
+
next_id: '299',
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
const transport = createUniplatBaseNotificationCenterTransport({
|
|
52
|
+
serviceBaseUrl: SERVICE_BASE,
|
|
53
|
+
getAccessToken: () => 'notify.jwt',
|
|
54
|
+
fetch: fetchMock as unknown as typeof fetch,
|
|
55
|
+
})
|
|
56
|
+
const result = await transport.listInbox({
|
|
57
|
+
pageSize: 20,
|
|
58
|
+
beforeId: null,
|
|
59
|
+
applicationName: 'HrSaas',
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(calls[0]?.url).toBe(`${SERVICE_BASE}/messaging.notification_api/inbox`)
|
|
63
|
+
const body = JSON.parse(String(calls[0]?.init.body))
|
|
64
|
+
expect(body.application_name).toBe('HrSaas')
|
|
65
|
+
expect(body.page_size).toBe(20)
|
|
66
|
+
const headers = calls[0]?.init.headers as Record<string, string>
|
|
67
|
+
expect(headers.authorization).toBe('Bearer notify.jwt')
|
|
68
|
+
expect(calls[0]?.url).not.toContain('notify.jwt')
|
|
69
|
+
|
|
70
|
+
expect(result.items).toHaveLength(1)
|
|
71
|
+
const item = result.items[0]
|
|
72
|
+
expect(item?.id).toBe('301')
|
|
73
|
+
// 分类展示名来自后端 notification_type_name,前端不再维护中文映射。
|
|
74
|
+
expect(item?.typeName).toBe('预警')
|
|
75
|
+
expect(item?.contentType).toBe('markdown')
|
|
76
|
+
expect(item?.resources[0]?.type).toBe('service_order')
|
|
77
|
+
expect(item?.attachments[0]?.fileId).toBe('f1')
|
|
78
|
+
expect(result.hasMore).toBe(true)
|
|
79
|
+
expect(result.nextId).toBe('299')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('requests include_type_facets and maps type_facets into the type catalog', async () => {
|
|
83
|
+
const calls: FakeCall[] = []
|
|
84
|
+
const fetchMock = vi.fn(async (url: string, init: RequestInit) => {
|
|
85
|
+
calls.push({ url, init })
|
|
86
|
+
return jsonResponse({
|
|
87
|
+
items: [],
|
|
88
|
+
has_more: false,
|
|
89
|
+
next_id: null,
|
|
90
|
+
type_facets: [
|
|
91
|
+
{ notification_type: 'alert', notification_type_name: '预警', total: 12, unread: 3 },
|
|
92
|
+
{ notification_type: 'sales_lead_assigned', notification_type_name: '新线索', total: 2 },
|
|
93
|
+
// 字典未登记的分类展示名为空,由 UI 回退到 code。
|
|
94
|
+
{ notification_type: 'interview', notification_type_name: '', total: 1 },
|
|
95
|
+
{ notification_type: '', notification_type_name: '脏数据', total: 9 },
|
|
96
|
+
],
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
const transport = createUniplatBaseNotificationCenterTransport({
|
|
100
|
+
serviceBaseUrl: SERVICE_BASE,
|
|
101
|
+
getAccessToken: () => 'notify.jwt',
|
|
102
|
+
fetch: fetchMock as unknown as typeof fetch,
|
|
103
|
+
})
|
|
104
|
+
const result = await transport.listInbox({
|
|
105
|
+
pageSize: 20,
|
|
106
|
+
beforeId: null,
|
|
107
|
+
includeTypeFacets: true,
|
|
108
|
+
applicationName: 'HrSaas',
|
|
109
|
+
})
|
|
110
|
+
const body = JSON.parse(String(calls[0]?.init.body))
|
|
111
|
+
expect(body.include_type_facets).toBe(true)
|
|
112
|
+
// 空 code 被丢弃;total/unread 不进入 UI 投影。
|
|
113
|
+
expect(result.types).toEqual([
|
|
114
|
+
{ type: 'alert', name: '预警' },
|
|
115
|
+
{ type: 'sales_lead_assigned', name: '新线索' },
|
|
116
|
+
{ type: 'interview', name: '' },
|
|
117
|
+
])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('omits include_type_facets and type catalog when not requested', async () => {
|
|
121
|
+
const calls: FakeCall[] = []
|
|
122
|
+
const fetchMock = vi.fn(async (url: string, init: RequestInit) => {
|
|
123
|
+
calls.push({ url, init })
|
|
124
|
+
return jsonResponse({ items: [], has_more: false, next_id: null })
|
|
125
|
+
})
|
|
126
|
+
const transport = createUniplatBaseNotificationCenterTransport({
|
|
127
|
+
serviceBaseUrl: SERVICE_BASE,
|
|
128
|
+
getAccessToken: () => 'notify.jwt',
|
|
129
|
+
fetch: fetchMock as unknown as typeof fetch,
|
|
130
|
+
})
|
|
131
|
+
const result = await transport.listInbox({
|
|
132
|
+
pageSize: 20,
|
|
133
|
+
beforeId: null,
|
|
134
|
+
applicationName: 'HrSaas',
|
|
135
|
+
})
|
|
136
|
+
expect(JSON.parse(String(calls[0]?.init.body))).not.toHaveProperty('include_type_facets')
|
|
137
|
+
expect(result.types).toBeUndefined()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('maps unread_count, mark_read and mark_all_read responses', async () => {
|
|
141
|
+
const fetchMock = vi.fn(async (url: string) => {
|
|
142
|
+
if (url.endsWith('/unread_count')) return jsonResponse({ unread_count: 7 })
|
|
143
|
+
if (url.endsWith('/mark_read'))
|
|
144
|
+
return jsonResponse({ unread_count: 6, read_at: '2026-05-30T17:00:00.000Z' })
|
|
145
|
+
if (url.endsWith('/mark_all_read')) return jsonResponse({ marked_count: 6, unread_count: 0 })
|
|
146
|
+
return jsonResponse({})
|
|
147
|
+
})
|
|
148
|
+
const transport = createUniplatBaseNotificationCenterTransport({
|
|
149
|
+
serviceBaseUrl: SERVICE_BASE,
|
|
150
|
+
getAccessToken: () => 'notify.jwt',
|
|
151
|
+
fetch: fetchMock as unknown as typeof fetch,
|
|
152
|
+
})
|
|
153
|
+
expect(await transport.getUnreadCount({ applicationName: 'HrSaas' })).toEqual({
|
|
154
|
+
unreadCount: 7,
|
|
155
|
+
})
|
|
156
|
+
expect(await transport.markRead({ notificationId: '1', applicationName: 'HrSaas' })).toEqual({
|
|
157
|
+
unreadCount: 6,
|
|
158
|
+
readAt: '2026-05-30T17:00:00.000Z',
|
|
159
|
+
})
|
|
160
|
+
expect(await transport.markAllRead({ applicationName: 'HrSaas' })).toEqual({
|
|
161
|
+
markedCount: 6,
|
|
162
|
+
unreadCount: 0,
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('omits application_name for the all-applications projection', async () => {
|
|
167
|
+
const calls: FakeCall[] = []
|
|
168
|
+
const fetchMock = vi.fn(async (url: string, init: RequestInit) => {
|
|
169
|
+
calls.push({ url, init })
|
|
170
|
+
if (url.endsWith('/inbox')) {
|
|
171
|
+
return jsonResponse({ items: [], has_more: false, next_id: null })
|
|
172
|
+
}
|
|
173
|
+
if (url.endsWith('/unread_count')) return jsonResponse({ unread_count: 2 })
|
|
174
|
+
if (url.endsWith('/mark_read')) return jsonResponse({ unread_count: 1, read_at: null })
|
|
175
|
+
if (url.endsWith('/mark_all_read')) return jsonResponse({ marked_count: 1, unread_count: 0 })
|
|
176
|
+
if (url.endsWith('/archive')) return jsonResponse({ unread_count: 0 })
|
|
177
|
+
return jsonResponse({})
|
|
178
|
+
})
|
|
179
|
+
const transport = createUniplatBaseNotificationCenterTransport({
|
|
180
|
+
serviceBaseUrl: SERVICE_BASE,
|
|
181
|
+
getAccessToken: () => 'notify.jwt',
|
|
182
|
+
fetch: fetchMock as unknown as typeof fetch,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await transport.listInbox({ pageSize: 4, applicationName: '' })
|
|
186
|
+
await transport.getUnreadCount({ applicationName: '' })
|
|
187
|
+
await transport.markRead({ notificationId: '1', applicationName: '' })
|
|
188
|
+
await transport.markAllRead({ applicationName: '' })
|
|
189
|
+
await transport.archive?.({ notificationId: '1', applicationName: '' })
|
|
190
|
+
|
|
191
|
+
expect(calls).toHaveLength(5)
|
|
192
|
+
for (const call of calls) {
|
|
193
|
+
expect(JSON.parse(String(call.init.body))).not.toHaveProperty('application_name')
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('downloads an attachment via a short-lived ticket, keeping the ticket out of the return value', async () => {
|
|
198
|
+
const calls: FakeCall[] = []
|
|
199
|
+
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
|
|
200
|
+
calls.push({ url, init: init ?? {} })
|
|
201
|
+
if (url.endsWith('/filesystem.api/create_download_ticket')) {
|
|
202
|
+
return jsonResponse({
|
|
203
|
+
ticket: 'secret-ticket',
|
|
204
|
+
download_url:
|
|
205
|
+
'/api/general/project/uniplat_base/service/anonymous/filesystem.api/download_by_ticket?ticket=secret-ticket',
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
async blob() {
|
|
211
|
+
return new Blob(['file-bytes'])
|
|
212
|
+
},
|
|
213
|
+
async json() {
|
|
214
|
+
return {}
|
|
215
|
+
},
|
|
216
|
+
} as unknown as Response
|
|
217
|
+
})
|
|
218
|
+
const transport = createUniplatBaseNotificationCenterTransport({
|
|
219
|
+
serviceBaseUrl: SERVICE_BASE,
|
|
220
|
+
getAccessToken: () => 'notify.jwt',
|
|
221
|
+
fetch: fetchMock as unknown as typeof fetch,
|
|
222
|
+
})
|
|
223
|
+
const blob = await transport.downloadAttachment({ fileId: 'f1', name: 'report.pdf' })
|
|
224
|
+
expect(blob).toBeInstanceOf(Blob)
|
|
225
|
+
|
|
226
|
+
const ticketCall = calls[0]
|
|
227
|
+
expect(ticketCall?.url).toBe(`${SERVICE_BASE}/filesystem.api/create_download_ticket`)
|
|
228
|
+
expect(JSON.parse(String(ticketCall?.init.body)).file_id).toBe('f1')
|
|
229
|
+
const downloadCall = calls[1]
|
|
230
|
+
expect(downloadCall?.url).toContain('download_by_ticket?ticket=secret-ticket')
|
|
231
|
+
// The download GET carries no Authorization header (anonymous ticket endpoint).
|
|
232
|
+
expect(
|
|
233
|
+
(downloadCall?.init.headers as Record<string, string> | undefined)?.authorization,
|
|
234
|
+
).toBeUndefined()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('parses realtime notification events into stable DTO changes', () => {
|
|
238
|
+
const transport = createUniplatBaseNotificationCenterTransport({
|
|
239
|
+
serviceBaseUrl: SERVICE_BASE,
|
|
240
|
+
getAccessToken: () => 'notify.jwt',
|
|
241
|
+
fetch: vi.fn() as unknown as typeof fetch,
|
|
242
|
+
})
|
|
243
|
+
const created = transport.parseRealtimeEvent?.({
|
|
244
|
+
event_type: 'notification_created',
|
|
245
|
+
unread_count: 5,
|
|
246
|
+
notification: {
|
|
247
|
+
notification_id: '9',
|
|
248
|
+
notification_type: 'todo',
|
|
249
|
+
title: '待办',
|
|
250
|
+
read_status: 'unread',
|
|
251
|
+
},
|
|
252
|
+
})
|
|
253
|
+
expect(created).toEqual(expect.objectContaining({ kind: 'created', unreadCount: 5 }))
|
|
254
|
+
const read = transport.parseRealtimeEvent?.({
|
|
255
|
+
event_type: 'notification_read',
|
|
256
|
+
notification_ids: ['9', '10'],
|
|
257
|
+
unread_count: 2,
|
|
258
|
+
})
|
|
259
|
+
expect(read).toEqual({
|
|
260
|
+
kind: 'read',
|
|
261
|
+
notificationIds: ['9', '10'],
|
|
262
|
+
unreadCount: 2,
|
|
263
|
+
readAt: null,
|
|
264
|
+
})
|
|
265
|
+
const archived = transport.parseRealtimeEvent?.({
|
|
266
|
+
event_type: 'notification_archived',
|
|
267
|
+
notification_id: '9',
|
|
268
|
+
})
|
|
269
|
+
expect(archived).toEqual({ kind: 'archived', notificationIds: ['9'], unreadCount: undefined })
|
|
270
|
+
})
|
|
271
|
+
})
|