@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,534 @@
|
|
|
1
|
+
import { LitElement, css, html, nothing, type TemplateResult } from 'lit'
|
|
2
|
+
|
|
3
|
+
export type OrganizationSelectorType = 'enterprise' | 'team'
|
|
4
|
+
export type EnterpriseVerificationStatus =
|
|
5
|
+
'unverified' | 'pending' | 'rejected' | 'verified' | 'expired'
|
|
6
|
+
|
|
7
|
+
export interface OrganizationSelectorEntry {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
type: OrganizationSelectorType
|
|
11
|
+
verificationStatus?: EnterpriseVerificationStatus
|
|
12
|
+
role?: string
|
|
13
|
+
memberCount?: number
|
|
14
|
+
lastEnteredLabel?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface OrganizationSelectorConfig {
|
|
18
|
+
organizations: readonly OrganizationSelectorEntry[]
|
|
19
|
+
selectedOrganizationId?: string
|
|
20
|
+
rememberSelection?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OrganizationSelectorEventMap {
|
|
24
|
+
'organization-enter-requested': CustomEvent<{
|
|
25
|
+
organizationId: string
|
|
26
|
+
rememberSelection: boolean
|
|
27
|
+
}>
|
|
28
|
+
'organization-create-requested': CustomEvent<Record<string, never>>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ResolvedOrganizationSelectorEntry {
|
|
32
|
+
id: string
|
|
33
|
+
name: string
|
|
34
|
+
type: OrganizationSelectorType
|
|
35
|
+
verificationStatus: EnterpriseVerificationStatus | null
|
|
36
|
+
role: string
|
|
37
|
+
memberCount: number | null
|
|
38
|
+
lastEnteredLabel: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class UniplatOrganizationSelector extends LitElement {
|
|
42
|
+
static override properties = {
|
|
43
|
+
organizations: { state: true },
|
|
44
|
+
selectedOrganizationId: { state: true },
|
|
45
|
+
rememberSelection: { state: true },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static override styles = css`
|
|
49
|
+
:host {
|
|
50
|
+
--uniplat-organization-selector-width: 400px;
|
|
51
|
+
--uniplat-organization-selector-primary: #2e6bf5;
|
|
52
|
+
--uniplat-organization-selector-text: #182634;
|
|
53
|
+
--uniplat-organization-selector-muted: #7a8794;
|
|
54
|
+
--uniplat-organization-selector-border: #e4ebf3;
|
|
55
|
+
--uniplat-organization-selector-font-family:
|
|
56
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
57
|
+
display: block;
|
|
58
|
+
width: min(var(--uniplat-organization-selector-width), 100%);
|
|
59
|
+
color: var(--uniplat-organization-selector-text);
|
|
60
|
+
font-family: var(--uniplat-organization-selector-font-family);
|
|
61
|
+
line-height: 1.5;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
* {
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
button,
|
|
69
|
+
input {
|
|
70
|
+
font: inherit;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
button {
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.card {
|
|
78
|
+
width: 100%;
|
|
79
|
+
padding: 24px;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
border: 1px solid var(--uniplat-organization-selector-border);
|
|
82
|
+
border-radius: 16px;
|
|
83
|
+
background: #ffffff;
|
|
84
|
+
box-shadow: 0 8px 24px rgba(20, 40, 80, 0.08);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
h2,
|
|
88
|
+
p {
|
|
89
|
+
margin: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
h2 {
|
|
93
|
+
font-size: 20px;
|
|
94
|
+
font-weight: 800;
|
|
95
|
+
line-height: 1.35;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.description {
|
|
99
|
+
margin-top: 7px;
|
|
100
|
+
color: var(--uniplat-organization-selector-muted);
|
|
101
|
+
font-size: 13px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.list {
|
|
105
|
+
display: grid;
|
|
106
|
+
gap: 9px;
|
|
107
|
+
max-height: 340px;
|
|
108
|
+
margin-top: 18px;
|
|
109
|
+
overflow-y: auto;
|
|
110
|
+
scrollbar-gutter: stable;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.organization {
|
|
114
|
+
display: grid;
|
|
115
|
+
grid-template-columns: 36px minmax(0, 1fr) 18px;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 10px;
|
|
118
|
+
width: 100%;
|
|
119
|
+
min-height: 62px;
|
|
120
|
+
padding: 10px 11px;
|
|
121
|
+
border: 1px solid #edf1f6;
|
|
122
|
+
border-radius: 10px;
|
|
123
|
+
color: inherit;
|
|
124
|
+
background: #ffffff;
|
|
125
|
+
text-align: left;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.organization:hover {
|
|
129
|
+
background: #fafcff;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.organization.selected {
|
|
133
|
+
border-color: var(--uniplat-organization-selector-primary);
|
|
134
|
+
background: #f7f9ff;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.organization-icon {
|
|
138
|
+
display: grid;
|
|
139
|
+
place-items: center;
|
|
140
|
+
width: 36px;
|
|
141
|
+
height: 36px;
|
|
142
|
+
border-radius: 9px;
|
|
143
|
+
color: var(--uniplat-organization-selector-primary);
|
|
144
|
+
background: #e6eefe;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.organization.team .organization-icon {
|
|
148
|
+
color: #0ca678;
|
|
149
|
+
background: #e3f7ef;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.copy {
|
|
153
|
+
min-width: 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.name-line {
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 6px;
|
|
160
|
+
min-width: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.name {
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
font-size: 13.5px;
|
|
166
|
+
font-weight: 750;
|
|
167
|
+
text-overflow: ellipsis;
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.badge {
|
|
172
|
+
flex: none;
|
|
173
|
+
padding: 1px 6px;
|
|
174
|
+
border-radius: 4px;
|
|
175
|
+
color: var(--uniplat-organization-selector-primary);
|
|
176
|
+
background: #e6eefe;
|
|
177
|
+
font-size: 10.5px;
|
|
178
|
+
font-weight: 700;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.badge.team {
|
|
182
|
+
color: #0ca678;
|
|
183
|
+
background: #e3f7ef;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.badge.unverified,
|
|
187
|
+
.badge.rejected,
|
|
188
|
+
.badge.expired {
|
|
189
|
+
color: #e8590c;
|
|
190
|
+
background: #fff4e2;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.badge.pending {
|
|
194
|
+
color: #1971c2;
|
|
195
|
+
background: #e2effb;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.meta {
|
|
199
|
+
display: block;
|
|
200
|
+
margin-top: 2px;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
color: #8b98a6;
|
|
203
|
+
font-size: 11.5px;
|
|
204
|
+
text-overflow: ellipsis;
|
|
205
|
+
white-space: nowrap;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.radio {
|
|
209
|
+
display: grid;
|
|
210
|
+
place-items: center;
|
|
211
|
+
width: 18px;
|
|
212
|
+
height: 18px;
|
|
213
|
+
border: 1.5px solid #c7d1df;
|
|
214
|
+
border-radius: 50%;
|
|
215
|
+
color: transparent;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.selected .radio {
|
|
219
|
+
border-color: var(--uniplat-organization-selector-primary);
|
|
220
|
+
color: #ffffff;
|
|
221
|
+
background: var(--uniplat-organization-selector-primary);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.footer-row {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: space-between;
|
|
228
|
+
gap: 12px;
|
|
229
|
+
margin-top: 15px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.remember {
|
|
233
|
+
display: inline-flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
gap: 7px;
|
|
236
|
+
color: #7a8794;
|
|
237
|
+
font-size: 12px;
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.remember input {
|
|
242
|
+
width: 15px;
|
|
243
|
+
height: 15px;
|
|
244
|
+
margin: 0;
|
|
245
|
+
accent-color: var(--uniplat-organization-selector-primary);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.create {
|
|
249
|
+
padding: 4px 0;
|
|
250
|
+
border: 0;
|
|
251
|
+
color: var(--uniplat-organization-selector-primary);
|
|
252
|
+
background: transparent;
|
|
253
|
+
font-size: 12.5px;
|
|
254
|
+
font-weight: 650;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.enter {
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
justify-content: center;
|
|
261
|
+
width: 100%;
|
|
262
|
+
min-height: 46px;
|
|
263
|
+
margin-top: 13px;
|
|
264
|
+
border: 0;
|
|
265
|
+
border-radius: 10px;
|
|
266
|
+
color: #ffffff;
|
|
267
|
+
background: var(--uniplat-organization-selector-primary);
|
|
268
|
+
font-size: 14px;
|
|
269
|
+
font-weight: 700;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.enter:disabled {
|
|
273
|
+
cursor: not-allowed;
|
|
274
|
+
background: #a9bff6;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.empty {
|
|
278
|
+
display: grid;
|
|
279
|
+
min-height: 120px;
|
|
280
|
+
place-items: center;
|
|
281
|
+
margin-top: 18px;
|
|
282
|
+
border: 1px dashed #dce5f0;
|
|
283
|
+
border-radius: 10px;
|
|
284
|
+
color: #8b98a6;
|
|
285
|
+
font-size: 13px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
svg {
|
|
289
|
+
display: block;
|
|
290
|
+
width: 20px;
|
|
291
|
+
height: 20px;
|
|
292
|
+
fill: none;
|
|
293
|
+
stroke: currentColor;
|
|
294
|
+
stroke-linecap: round;
|
|
295
|
+
stroke-linejoin: round;
|
|
296
|
+
stroke-width: 1.8;
|
|
297
|
+
}
|
|
298
|
+
`
|
|
299
|
+
|
|
300
|
+
declare private organizations: readonly ResolvedOrganizationSelectorEntry[]
|
|
301
|
+
declare private selectedOrganizationId: string
|
|
302
|
+
declare private rememberSelection: boolean
|
|
303
|
+
|
|
304
|
+
private configured = false
|
|
305
|
+
|
|
306
|
+
constructor() {
|
|
307
|
+
super()
|
|
308
|
+
this.organizations = []
|
|
309
|
+
this.selectedOrganizationId = ''
|
|
310
|
+
this.rememberSelection = true
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
configure(config: OrganizationSelectorConfig): void {
|
|
314
|
+
if (!config || !Array.isArray(config.organizations)) {
|
|
315
|
+
throw new TypeError('organization-selector requires an organizations array')
|
|
316
|
+
}
|
|
317
|
+
const organizations = normalizeOrganizations(config.organizations)
|
|
318
|
+
const requestedSelection = normalizeOptionalText(config.selectedOrganizationId, 128)
|
|
319
|
+
this.organizations = organizations
|
|
320
|
+
this.selectedOrganizationId = organizations.some(({ id }) => id === requestedSelection)
|
|
321
|
+
? requestedSelection
|
|
322
|
+
: (organizations[0]?.id ?? '')
|
|
323
|
+
this.rememberSelection = config.rememberSelection ?? true
|
|
324
|
+
this.configured = true
|
|
325
|
+
this.requestUpdate()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
override render() {
|
|
329
|
+
if (!this.configured) return nothing
|
|
330
|
+
return html`
|
|
331
|
+
<section class="card" part="shell" aria-label="选择团队或企业">
|
|
332
|
+
<h2 part="title">选择要进入的团队 / 企业</h2>
|
|
333
|
+
<p class="description" part="description">
|
|
334
|
+
你归属 ${this.organizations.length} 个团队 / 企业,可随时在导航中切换
|
|
335
|
+
</p>
|
|
336
|
+
${
|
|
337
|
+
this.organizations.length
|
|
338
|
+
? html`<div class="list" part="organization-list">
|
|
339
|
+
${this.organizations.map((organization) => this.renderOrganization(organization))}
|
|
340
|
+
</div>`
|
|
341
|
+
: html`<div class="empty" part="empty">暂无可进入的团队或企业</div>`
|
|
342
|
+
}
|
|
343
|
+
<div class="footer-row">
|
|
344
|
+
<label class="remember" part="remember-selection">
|
|
345
|
+
<input
|
|
346
|
+
type="checkbox"
|
|
347
|
+
.checked=${this.rememberSelection}
|
|
348
|
+
@change=${(event: Event) => {
|
|
349
|
+
this.rememberSelection = (event.currentTarget as HTMLInputElement).checked
|
|
350
|
+
}}
|
|
351
|
+
/>
|
|
352
|
+
记住选择,下次直接进入
|
|
353
|
+
</label>
|
|
354
|
+
<button class="create" part="create-action" type="button" @click=${this.requestCreate}>
|
|
355
|
+
+ 创建
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
358
|
+
<button
|
|
359
|
+
class="enter"
|
|
360
|
+
part="enter-action"
|
|
361
|
+
type="button"
|
|
362
|
+
?disabled=${!this.selectedOrganizationId}
|
|
363
|
+
@click=${this.requestEnter}
|
|
364
|
+
>
|
|
365
|
+
进入
|
|
366
|
+
</button>
|
|
367
|
+
</section>
|
|
368
|
+
`
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private renderOrganization(organization: ResolvedOrganizationSelectorEntry) {
|
|
372
|
+
const selected = organization.id === this.selectedOrganizationId
|
|
373
|
+
const badge = organizationBadge(organization)
|
|
374
|
+
return html`
|
|
375
|
+
<button
|
|
376
|
+
class="organization ${organization.type === 'team' ? 'team' : ''} ${
|
|
377
|
+
selected ? 'selected' : ''
|
|
378
|
+
}"
|
|
379
|
+
part="organization"
|
|
380
|
+
type="button"
|
|
381
|
+
aria-pressed=${String(selected)}
|
|
382
|
+
@click=${() => {
|
|
383
|
+
this.selectedOrganizationId = organization.id
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
<span class="organization-icon">${icon(organization.type)}</span>
|
|
387
|
+
<span class="copy">
|
|
388
|
+
<span class="name-line">
|
|
389
|
+
<span class="name">${organization.name}</span>
|
|
390
|
+
${
|
|
391
|
+
badge
|
|
392
|
+
? html`<span class="badge ${badge.className}" part="organization-badge">
|
|
393
|
+
${badge.label}
|
|
394
|
+
</span>`
|
|
395
|
+
: nothing
|
|
396
|
+
}
|
|
397
|
+
</span>
|
|
398
|
+
<span class="meta">${organizationMeta(organization)}</span>
|
|
399
|
+
</span>
|
|
400
|
+
<span class="radio">${icon('check')}</span>
|
|
401
|
+
</button>
|
|
402
|
+
`
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private readonly requestCreate = () => {
|
|
406
|
+
this.dispatchEvent(
|
|
407
|
+
new CustomEvent('organization-create-requested', {
|
|
408
|
+
detail: {},
|
|
409
|
+
bubbles: true,
|
|
410
|
+
composed: true,
|
|
411
|
+
}),
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private readonly requestEnter = () => {
|
|
416
|
+
if (!this.selectedOrganizationId) return
|
|
417
|
+
this.dispatchEvent(
|
|
418
|
+
new CustomEvent('organization-enter-requested', {
|
|
419
|
+
detail: {
|
|
420
|
+
organizationId: this.selectedOrganizationId,
|
|
421
|
+
rememberSelection: this.rememberSelection,
|
|
422
|
+
},
|
|
423
|
+
bubbles: true,
|
|
424
|
+
composed: true,
|
|
425
|
+
}),
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function normalizeOrganizations(
|
|
431
|
+
organizations: readonly OrganizationSelectorEntry[],
|
|
432
|
+
): readonly ResolvedOrganizationSelectorEntry[] {
|
|
433
|
+
const seen = new Set<string>()
|
|
434
|
+
return organizations.map((organization) => {
|
|
435
|
+
if (!organization || typeof organization !== 'object') {
|
|
436
|
+
throw new TypeError('organization-selector entries must be objects')
|
|
437
|
+
}
|
|
438
|
+
const id = normalizeRequiredText(organization.id, 'Organization id', 128)
|
|
439
|
+
if (seen.has(id)) throw new TypeError('organization-selector organization ids must be unique')
|
|
440
|
+
seen.add(id)
|
|
441
|
+
const type = organization.type
|
|
442
|
+
if (!['enterprise', 'team'].includes(type)) {
|
|
443
|
+
throw new TypeError('organization-selector organization type is invalid')
|
|
444
|
+
}
|
|
445
|
+
const verificationStatus = normalizeVerificationStatus(type, organization.verificationStatus)
|
|
446
|
+
return {
|
|
447
|
+
id,
|
|
448
|
+
name: normalizeRequiredText(organization.name, 'Organization name', 100),
|
|
449
|
+
type,
|
|
450
|
+
verificationStatus,
|
|
451
|
+
role: normalizeOptionalText(organization.role, 60),
|
|
452
|
+
memberCount: normalizeMemberCount(organization.memberCount),
|
|
453
|
+
lastEnteredLabel: normalizeOptionalText(organization.lastEnteredLabel, 80),
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function normalizeVerificationStatus(
|
|
459
|
+
type: OrganizationSelectorType,
|
|
460
|
+
value: EnterpriseVerificationStatus | undefined,
|
|
461
|
+
): EnterpriseVerificationStatus | null {
|
|
462
|
+
if (type === 'team') return null
|
|
463
|
+
if (value === undefined) return 'verified'
|
|
464
|
+
if (!['unverified', 'pending', 'rejected', 'verified', 'expired'].includes(value)) {
|
|
465
|
+
throw new TypeError('organization-selector verification status is invalid')
|
|
466
|
+
}
|
|
467
|
+
return value
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function normalizeMemberCount(value: unknown): number | null {
|
|
471
|
+
if (value === undefined || value === null) return null
|
|
472
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
473
|
+
throw new TypeError('Organization member count must be a non-negative integer')
|
|
474
|
+
}
|
|
475
|
+
return value
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function normalizeRequiredText(value: unknown, label: string, maxLength: number): string {
|
|
479
|
+
const normalized = normalizeOptionalText(value, maxLength)
|
|
480
|
+
if (!normalized) throw new TypeError(`${label} is required`)
|
|
481
|
+
return normalized
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function normalizeOptionalText(value: unknown, maxLength: number): string {
|
|
485
|
+
if (value === undefined || value === null) return ''
|
|
486
|
+
if (typeof value !== 'string')
|
|
487
|
+
throw new TypeError('organization-selector text values must be strings')
|
|
488
|
+
const normalized = value.trim()
|
|
489
|
+
if (normalized.length > maxLength) {
|
|
490
|
+
throw new TypeError('organization-selector text value is too long')
|
|
491
|
+
}
|
|
492
|
+
return normalized
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function organizationMeta(organization: ResolvedOrganizationSelectorEntry): string {
|
|
496
|
+
const values: string[] = []
|
|
497
|
+
if (organization.role) values.push(organization.role)
|
|
498
|
+
if (organization.memberCount !== null) values.push(`${organization.memberCount} 人`)
|
|
499
|
+
if (organization.lastEnteredLabel) values.push(organization.lastEnteredLabel)
|
|
500
|
+
return values.join(' · ')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function organizationBadge(organization: ResolvedOrganizationSelectorEntry) {
|
|
504
|
+
if (organization.type === 'team') return { label: '团队', className: 'team' }
|
|
505
|
+
if (organization.verificationStatus === 'unverified') {
|
|
506
|
+
return { label: '未认证', className: 'unverified' }
|
|
507
|
+
}
|
|
508
|
+
if (organization.verificationStatus === 'pending') {
|
|
509
|
+
return { label: '审核中', className: 'pending' }
|
|
510
|
+
}
|
|
511
|
+
if (organization.verificationStatus === 'rejected') {
|
|
512
|
+
return { label: '未通过', className: 'rejected' }
|
|
513
|
+
}
|
|
514
|
+
if (organization.verificationStatus === 'expired') {
|
|
515
|
+
return { label: '已过期', className: 'expired' }
|
|
516
|
+
}
|
|
517
|
+
return { label: '企业', className: 'enterprise' }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function icon(name: OrganizationSelectorType | 'check'): TemplateResult {
|
|
521
|
+
if (name === 'enterprise') {
|
|
522
|
+
return html`<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
523
|
+
<path d="M5 21V6l7-3 7 3v15M3 21h18" />
|
|
524
|
+
<path d="M9 8h1M14 8h1M9 12h1M14 12h1M9 16h1M14 16h1" />
|
|
525
|
+
</svg>`
|
|
526
|
+
}
|
|
527
|
+
if (name === 'team') {
|
|
528
|
+
return html`<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
529
|
+
<circle cx="9" cy="8" r="3" />
|
|
530
|
+
<path d="M3.5 20v-2a5.5 5.5 0 0 1 11 0v2M16 5.5a3 3 0 0 1 0 5.5M17 14a5 5 0 0 1 3.5 4.8V20" />
|
|
531
|
+
</svg>`
|
|
532
|
+
}
|
|
533
|
+
return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4 10-10" /></svg>`
|
|
534
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UniplatOrganizationMultiEmployeeSelectorElement,
|
|
3
|
+
type OrganizationEmployeeSelectionItem,
|
|
4
|
+
type OrganizationMultiEmployeeSelectorDisabledSelection,
|
|
5
|
+
type OrganizationMultiEmployeeSelectorErrorDetail,
|
|
6
|
+
type UniplatOrganizationMultiEmployeeSelectorTransport,
|
|
7
|
+
} from './organization-multi-employee-selector'
|
|
8
|
+
|
|
9
|
+
export type UniplatOrganizationSingleEmployeeSelectorTransport =
|
|
10
|
+
UniplatOrganizationMultiEmployeeSelectorTransport
|
|
11
|
+
|
|
12
|
+
export interface OrganizationSingleEmployeeSelectorConfig {
|
|
13
|
+
applicationName: string
|
|
14
|
+
transport: UniplatOrganizationSingleEmployeeSelectorTransport
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type OrganizationSingleEmployeeSelectorDisabledSelection =
|
|
18
|
+
OrganizationMultiEmployeeSelectorDisabledSelection
|
|
19
|
+
|
|
20
|
+
export interface OrganizationSingleEmployeeSelectorOpenOptions {
|
|
21
|
+
title?: string
|
|
22
|
+
initialSelectedEmployeeId?: string
|
|
23
|
+
disabledSelections?: readonly OrganizationSingleEmployeeSelectorDisabledSelection[]
|
|
24
|
+
selectableEmploymentStatuses?: readonly 'active'[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OrganizationSingleEmployeeSelectionConfirmedDetail {
|
|
28
|
+
employeeId: string
|
|
29
|
+
employee: OrganizationEmployeeSelectionItem
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type OrganizationSingleEmployeeSelectorErrorDetail =
|
|
33
|
+
OrganizationMultiEmployeeSelectorErrorDetail
|
|
34
|
+
|
|
35
|
+
export interface OrganizationSingleEmployeeSelectorEventMap {
|
|
36
|
+
'selection-confirmed': CustomEvent<OrganizationSingleEmployeeSelectionConfirmedDetail>
|
|
37
|
+
'selection-cancelled': CustomEvent<Record<string, never>>
|
|
38
|
+
'selector-error': CustomEvent<OrganizationSingleEmployeeSelectorErrorDetail>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class UniplatOrganizationSingleEmployeeSelectorElement extends UniplatOrganizationMultiEmployeeSelectorElement {
|
|
42
|
+
protected override get singleSelection(): boolean {
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override configure(config: OrganizationSingleEmployeeSelectorConfig): void {
|
|
47
|
+
super.configure(config)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override addEventListener<K extends keyof OrganizationSingleEmployeeSelectorEventMap>(
|
|
51
|
+
type: K,
|
|
52
|
+
listener: (
|
|
53
|
+
this: UniplatOrganizationSingleEmployeeSelectorElement,
|
|
54
|
+
event: OrganizationSingleEmployeeSelectorEventMap[K],
|
|
55
|
+
) => unknown,
|
|
56
|
+
options?: boolean | AddEventListenerOptions,
|
|
57
|
+
): void
|
|
58
|
+
override addEventListener(
|
|
59
|
+
type: string,
|
|
60
|
+
listener: EventListenerOrEventListenerObject,
|
|
61
|
+
options?: boolean | AddEventListenerOptions,
|
|
62
|
+
): void
|
|
63
|
+
override addEventListener(
|
|
64
|
+
type: string,
|
|
65
|
+
listener: EventListenerOrEventListenerObject,
|
|
66
|
+
options?: boolean | AddEventListenerOptions,
|
|
67
|
+
): void {
|
|
68
|
+
super.addEventListener(type, listener, options)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override open(options: OrganizationSingleEmployeeSelectorOpenOptions = {}): Promise<void> {
|
|
72
|
+
validateSingleOpenOptions(options)
|
|
73
|
+
return super.open({
|
|
74
|
+
...(options.title === undefined ? {} : { title: options.title }),
|
|
75
|
+
initialSelectedEmployeeIds:
|
|
76
|
+
options.initialSelectedEmployeeId === undefined ? [] : [options.initialSelectedEmployeeId],
|
|
77
|
+
...(options.disabledSelections === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { disabledSelections: options.disabledSelections }),
|
|
80
|
+
...(options.selectableEmploymentStatuses === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { selectableEmploymentStatuses: options.selectableEmploymentStatuses }),
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected override dispatchSelectionConfirmed(
|
|
87
|
+
employees: OrganizationEmployeeSelectionItem[],
|
|
88
|
+
): void {
|
|
89
|
+
const employee = employees[0]
|
|
90
|
+
if (!employee) return
|
|
91
|
+
this.dispatchEvent(
|
|
92
|
+
new CustomEvent<OrganizationSingleEmployeeSelectionConfirmedDetail>('selection-confirmed', {
|
|
93
|
+
detail: { employeeId: employee.employeeId, employee },
|
|
94
|
+
bubbles: true,
|
|
95
|
+
composed: true,
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateSingleOpenOptions(options: OrganizationSingleEmployeeSelectorOpenOptions): void {
|
|
102
|
+
if (!options || typeof options !== 'object') {
|
|
103
|
+
throw new TypeError('Organization single employee selector open options are invalid')
|
|
104
|
+
}
|
|
105
|
+
const unsupported = options as OrganizationSingleEmployeeSelectorOpenOptions &
|
|
106
|
+
Record<string, unknown>
|
|
107
|
+
if (
|
|
108
|
+
'initialSelectedEmployeeIds' in unsupported ||
|
|
109
|
+
'maxSelection' in unsupported ||
|
|
110
|
+
'enableDepartmentBulkSelection' in unsupported
|
|
111
|
+
) {
|
|
112
|
+
throw new TypeError('Multi employee selector options are not supported by the single selector')
|
|
113
|
+
}
|
|
114
|
+
}
|