central-auth-js-sdk 1.2.10 → 1.2.11
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/INTEGRATION_GUIDE.md +16 -2
- package/auth.js +8 -0
- package/core/api.js +27 -0
- package/package.json +1 -1
- package/signup.js +29 -0
- package/styles.css +25 -0
- package/ui/screens/email-verify.js +43 -71
- package/ui/screens/index.js +47 -28
- package/ui/screens/org-details.js +32 -61
- package/ui/screens/org-fields.js +190 -0
package/INTEGRATION_GUIDE.md
CHANGED
|
@@ -354,9 +354,13 @@ const instance = startSignup({
|
|
|
354
354
|
|
|
355
355
|
## 5.3. Organization details (Team, Sub-team & Designation)
|
|
356
356
|
|
|
357
|
-
During
|
|
357
|
+
During registration (both email signup verification and Google/Social profile completion), the SDK renders **dynamic dropdown menus** populated with the **Teams**, **Sub-teams**, and **Designations** configured in the Central Auth Admin Interface for your entity.
|
|
358
358
|
|
|
359
|
-
|
|
359
|
+
### Dynamic Dropdown & Hierarchy Features
|
|
360
|
+
- **Dynamic Hierarchy**: The **Sub-team** dropdown dynamically updates its options according to the selected **Team** (displaying only sub-teams mapped to that parent team).
|
|
361
|
+
- **Auto-Resolution**: By default, the SDK automatically detects the entity domain from the user's email (e.g. `jane@jukshio.com` -> `jukshio.com`) and queries `GET /org/structure?entity_domain=...`.
|
|
362
|
+
- **Explicit Entity Configuration**: You can explicitly supply `entityName` (e.g. `'Juksho'`), `entityId`, or `entityDomain` in `startSignup` or `initializeAuth` to bind the signup wizard to a specific tenant.
|
|
363
|
+
- **Granular Visibility**: If your application does not need to collect organization details, or if you only want to collect a subset of them, use **`collectOrgDetails`** (or its alias **`showOrgDetails`**).
|
|
360
364
|
|
|
361
365
|
### Configuration options
|
|
362
366
|
|
|
@@ -392,6 +396,16 @@ const instance = startSignup({
|
|
|
392
396
|
});
|
|
393
397
|
```
|
|
394
398
|
|
|
399
|
+
#### 3. Bind to an entity explicitly (e.g. Juksho)
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
const instance = startSignup({
|
|
403
|
+
container: '#auth-container',
|
|
404
|
+
env: 'dev',
|
|
405
|
+
entityName: 'Juksho', // Loads teams, sub-teams, and designations for Juksho entity
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
395
409
|
#### 3. Set globally via `initializeAuth`
|
|
396
410
|
|
|
397
411
|
```javascript
|
package/auth.js
CHANGED
|
@@ -125,6 +125,10 @@ export function initializeAuth(config = {}) {
|
|
|
125
125
|
collectOrgDetails: true,
|
|
126
126
|
mobileVerification: 'india_only',
|
|
127
127
|
skipMobileOtp: false,
|
|
128
|
+
entityName: null,
|
|
129
|
+
entityId: null,
|
|
130
|
+
entityDomain: null,
|
|
131
|
+
orgStructure: null,
|
|
128
132
|
theme: null,
|
|
129
133
|
...config,
|
|
130
134
|
};
|
|
@@ -187,6 +191,10 @@ export function startSignup(config = {}) {
|
|
|
187
191
|
showOrgDetails: config.showOrgDetails ?? merged.showOrgDetails,
|
|
188
192
|
mobileVerification: config.mobileVerification ?? merged.mobileVerification,
|
|
189
193
|
skipMobileOtp: config.skipMobileOtp ?? merged.skipMobileOtp,
|
|
194
|
+
entityName: config.entityName ?? merged.entityName,
|
|
195
|
+
entityId: config.entityId ?? merged.entityId,
|
|
196
|
+
entityDomain: config.entityDomain ?? merged.entityDomain,
|
|
197
|
+
orgStructure: config.orgStructure ?? merged.orgStructure,
|
|
190
198
|
theme: merged.theme || undefined,
|
|
191
199
|
onSignupComplete: config.onSignupComplete ?? merged.onSignupComplete,
|
|
192
200
|
onContinue: config.onContinue ?? merged.onContinue,
|
package/core/api.js
CHANGED
|
@@ -94,6 +94,33 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
|
|
|
94
94
|
return request('POST', '/signup/org/details', payload);
|
|
95
95
|
},
|
|
96
96
|
|
|
97
|
+
/** GET /org/structure — Fetch organization structure (teams, sub-teams, designations) */
|
|
98
|
+
async getOrgStructure({ entityId, entityName, entityDomain } = {}) {
|
|
99
|
+
const tryFetch = async (queryParam) => {
|
|
100
|
+
return request('GET', `/org/structure?${queryParam}`);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (entityId) {
|
|
104
|
+
try {
|
|
105
|
+
const res = await tryFetch(`entity_id=${encodeURIComponent(entityId)}`);
|
|
106
|
+
if (res && (res.teams || res.designations)) return res;
|
|
107
|
+
} catch (_) {}
|
|
108
|
+
}
|
|
109
|
+
if (entityDomain) {
|
|
110
|
+
try {
|
|
111
|
+
const res = await tryFetch(`entity_domain=${encodeURIComponent(entityDomain)}`);
|
|
112
|
+
if (res && (res.teams || res.designations)) return res;
|
|
113
|
+
} catch (_) {}
|
|
114
|
+
}
|
|
115
|
+
if (entityName) {
|
|
116
|
+
try {
|
|
117
|
+
const res = await tryFetch(`entity_name=${encodeURIComponent(entityName)}`);
|
|
118
|
+
if (res && (res.teams || res.designations)) return res;
|
|
119
|
+
} catch (_) {}
|
|
120
|
+
}
|
|
121
|
+
return { teams: [], designations: [] };
|
|
122
|
+
},
|
|
123
|
+
|
|
97
124
|
/** POST /signup/password/set — Password step */
|
|
98
125
|
async signupPasswordSet(email, password) {
|
|
99
126
|
return request('POST', '/signup/password/set', { email, password });
|
package/package.json
CHANGED
package/signup.js
CHANGED
|
@@ -74,6 +74,10 @@ export function mount(container, options = {}) {
|
|
|
74
74
|
mobileVerification = 'india_only',
|
|
75
75
|
collectOrgDetails = true,
|
|
76
76
|
showOrgDetails,
|
|
77
|
+
entityName = null,
|
|
78
|
+
entityId = null,
|
|
79
|
+
entityDomain = null,
|
|
80
|
+
orgStructure = null,
|
|
77
81
|
} = options;
|
|
78
82
|
|
|
79
83
|
const flow = new FlowController();
|
|
@@ -84,6 +88,10 @@ export function mount(container, options = {}) {
|
|
|
84
88
|
if (showOrgDetails !== undefined) {
|
|
85
89
|
state.set('showOrgDetails', showOrgDetails);
|
|
86
90
|
}
|
|
91
|
+
if (entityName) state.set('entityName', entityName);
|
|
92
|
+
if (entityId) state.set('entityId', entityId);
|
|
93
|
+
if (entityDomain) state.set('entityDomain', entityDomain);
|
|
94
|
+
if (orgStructure) state.set('orgStructure', orgStructure);
|
|
87
95
|
const api = createApi(apiBaseUrl);
|
|
88
96
|
|
|
89
97
|
const layout = createLayout(el, () => {});
|
|
@@ -96,7 +104,18 @@ export function mount(container, options = {}) {
|
|
|
96
104
|
state.set('loginToken', oauth.loginToken);
|
|
97
105
|
state.set('socialProvider', oauth.provider);
|
|
98
106
|
if (oauth.email) state.set('email', oauth.email);
|
|
107
|
+
if (oauth.entityDomain) state.set('entityDomain', oauth.entityDomain);
|
|
99
108
|
clearOAuthParams();
|
|
109
|
+
const socialDomain = oauth.entityDomain || (oauth.email && oauth.email.includes('@') ? oauth.email.split('@')[1] : null);
|
|
110
|
+
api.getOrgStructure({
|
|
111
|
+
entityId: state.get('entityId'),
|
|
112
|
+
entityName: state.get('entityName'),
|
|
113
|
+
entityDomain: socialDomain,
|
|
114
|
+
}).then((res) => {
|
|
115
|
+
if (res && (res.teams || res.designations)) {
|
|
116
|
+
state.set('orgStructure', res);
|
|
117
|
+
}
|
|
118
|
+
}).catch(() => {});
|
|
100
119
|
flow.setSocialFlow();
|
|
101
120
|
if (oauth.nextStep === 'FR_ENROLLMENT') {
|
|
102
121
|
flow.setStep(STEPS.FACE);
|
|
@@ -131,6 +150,16 @@ export function mount(container, options = {}) {
|
|
|
131
150
|
if (data.lastName) state.set('lastName', data.lastName);
|
|
132
151
|
}
|
|
133
152
|
state.set('email', email);
|
|
153
|
+
const emailDomain = state.get('entityDomain') || (email && email.includes('@') ? email.split('@')[1] : null);
|
|
154
|
+
api.getOrgStructure({
|
|
155
|
+
entityId: state.get('entityId'),
|
|
156
|
+
entityName: state.get('entityName'),
|
|
157
|
+
entityDomain: emailDomain,
|
|
158
|
+
}).then((res) => {
|
|
159
|
+
if (res && (res.teams || res.designations)) {
|
|
160
|
+
state.set('orgStructure', res);
|
|
161
|
+
}
|
|
162
|
+
}).catch(() => {});
|
|
134
163
|
try {
|
|
135
164
|
const res = await api.signupStart(email);
|
|
136
165
|
const apiStep = mapNextStep(res?.next_step);
|
package/styles.css
CHANGED
|
@@ -557,6 +557,31 @@
|
|
|
557
557
|
box-shadow: 0 0 0 3px var(--wf-primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
select.wf-input,
|
|
561
|
+
.wf-select {
|
|
562
|
+
appearance: none;
|
|
563
|
+
-webkit-appearance: none;
|
|
564
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
|
|
565
|
+
background-repeat: no-repeat;
|
|
566
|
+
background-position: right 0.875rem center;
|
|
567
|
+
background-size: 1rem 1rem;
|
|
568
|
+
padding-right: 2.25rem;
|
|
569
|
+
cursor: pointer;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
select.wf-input option,
|
|
573
|
+
.wf-select option {
|
|
574
|
+
background: var(--wf-card);
|
|
575
|
+
color: var(--wf-fg);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
select.wf-input:disabled,
|
|
579
|
+
.wf-select:disabled {
|
|
580
|
+
opacity: 0.6;
|
|
581
|
+
cursor: not-allowed;
|
|
582
|
+
background-color: var(--wf-muted-bg);
|
|
583
|
+
}
|
|
584
|
+
|
|
560
585
|
.wf-input::placeholder {
|
|
561
586
|
color: var(--wf-muted);
|
|
562
587
|
}
|
|
@@ -1,63 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Email Verification Screen — OTP + First/Last name
|
|
2
|
+
* Email Verification Screen — OTP + First/Last name + Org Hierarchy Dropdowns
|
|
3
3
|
* v2.0 — warm, modern design
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
6
|
+
import { renderOrgFieldsHtml, attachOrgFieldsLogic } from "./org-fields.js";
|
|
7
|
+
|
|
8
|
+
const MAIL_CHECK = "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z";
|
|
9
|
+
const ARROW_LEFT = "M19 12H5m7 7l-7-7 7-7";
|
|
10
|
+
const ARROW_RIGHT = "M5 12h14m-7-7l7 7-7 7";
|
|
9
11
|
|
|
10
12
|
export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}, options = {}) {
|
|
11
|
-
const wrap = document.createElement(
|
|
12
|
-
wrap.className =
|
|
13
|
+
const wrap = document.createElement("div");
|
|
14
|
+
wrap.className = "wf-screen";
|
|
13
15
|
|
|
14
16
|
const orgOpt = options.collectOrgDetails !== undefined ? options.collectOrgDetails : options.showOrgDetails;
|
|
15
|
-
const showTeam = typeof orgOpt ===
|
|
16
|
-
const showSubTeam = typeof orgOpt ===
|
|
17
|
-
const showDesignation = typeof orgOpt ===
|
|
17
|
+
const showTeam = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.team) : orgOpt !== false;
|
|
18
|
+
const showSubTeam = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.subTeam) : orgOpt !== false;
|
|
19
|
+
const showDesignation = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.designation) : orgOpt !== false;
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<input type="text" id="wf-team" placeholder="Engineering" class="wf-input" autocomplete="organization" />
|
|
26
|
-
</div>
|
|
27
|
-
<div class="wf-field">
|
|
28
|
-
<label class="wf-label" for="wf-sub-team">Sub-team</label>
|
|
29
|
-
<input type="text" id="wf-sub-team" placeholder="Backend" class="wf-input" />
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
`;
|
|
33
|
-
} else if (showTeam) {
|
|
34
|
-
orgFieldsHtml += `
|
|
35
|
-
<div class="wf-field">
|
|
36
|
-
<label class="wf-label" for="wf-team">Team</label>
|
|
37
|
-
<input type="text" id="wf-team" placeholder="Engineering" class="wf-input" autocomplete="organization" />
|
|
38
|
-
</div>
|
|
39
|
-
`;
|
|
40
|
-
} else if (showSubTeam) {
|
|
41
|
-
orgFieldsHtml += `
|
|
42
|
-
<div class="wf-field">
|
|
43
|
-
<label class="wf-label" for="wf-sub-team">Sub-team</label>
|
|
44
|
-
<input type="text" id="wf-sub-team" placeholder="Backend" class="wf-input" />
|
|
45
|
-
</div>
|
|
46
|
-
`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (showDesignation) {
|
|
50
|
-
orgFieldsHtml += `
|
|
51
|
-
<div class="wf-field">
|
|
52
|
-
<label class="wf-label" for="wf-designation">Designation</label>
|
|
53
|
-
<input type="text" id="wf-designation" placeholder="Software Engineer" class="wf-input" autocomplete="organization-title" />
|
|
54
|
-
</div>
|
|
55
|
-
`;
|
|
56
|
-
}
|
|
21
|
+
const orgFieldsHtml = renderOrgFieldsHtml({
|
|
22
|
+
showTeam,
|
|
23
|
+
showSubTeam,
|
|
24
|
+
showDesignation,
|
|
25
|
+
prefix: "wf-",
|
|
26
|
+
});
|
|
57
27
|
|
|
58
28
|
const otpInputs = Array(6).fill(0).map((_, i) => `
|
|
59
29
|
<input type="text" inputmode="numeric" maxlength="1" class="wf-otp-input" id="wf-email-otp-${i}" data-idx="${i}" autocomplete="one-time-code" />
|
|
60
|
-
`).join(
|
|
30
|
+
`).join("");
|
|
61
31
|
|
|
62
32
|
wrap.innerHTML = `
|
|
63
33
|
<div class="wf-screen-header">
|
|
@@ -98,57 +68,59 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}, opt
|
|
|
98
68
|
</p>
|
|
99
69
|
`;
|
|
100
70
|
|
|
101
|
-
const form = wrap.querySelector(
|
|
102
|
-
const otpEls = wrap.querySelectorAll(
|
|
103
|
-
const firstName = wrap.querySelector(
|
|
104
|
-
const lastName = wrap.querySelector(
|
|
105
|
-
const team = wrap.querySelector('#wf-team');
|
|
106
|
-
const subTeam = wrap.querySelector('#wf-sub-team');
|
|
107
|
-
const designation = wrap.querySelector('#wf-designation');
|
|
71
|
+
const form = wrap.querySelector("#wf-email-verify-form");
|
|
72
|
+
const otpEls = wrap.querySelectorAll(".wf-otp-input");
|
|
73
|
+
const firstName = wrap.querySelector("#wf-first-name");
|
|
74
|
+
const lastName = wrap.querySelector("#wf-last-name");
|
|
108
75
|
|
|
109
76
|
if (prefill.firstName) firstName.value = prefill.firstName;
|
|
110
77
|
if (prefill.lastName) lastName.value = prefill.lastName;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
78
|
+
|
|
79
|
+
// Initialize and attach dynamic org dropdowns logic
|
|
80
|
+
const orgLogic = attachOrgFieldsLogic({
|
|
81
|
+
wrap,
|
|
82
|
+
prefix: "wf-",
|
|
83
|
+
prefill,
|
|
84
|
+
options,
|
|
85
|
+
});
|
|
114
86
|
|
|
115
87
|
function handleOtpPaste(e) {
|
|
116
88
|
e.preventDefault();
|
|
117
|
-
const pasted = (e.clipboardData || window.clipboardData).getData(
|
|
118
|
-
const digits = pasted.replace(/\D/g,
|
|
89
|
+
const pasted = (e.clipboardData || window.clipboardData).getData("text");
|
|
90
|
+
const digits = pasted.replace(/\D/g, "").slice(0, 6).split("");
|
|
119
91
|
digits.forEach((d, i) => { if (otpEls[i]) otpEls[i].value = d; });
|
|
120
92
|
const nextIdx = Math.min(digits.length, 5);
|
|
121
93
|
if (otpEls[nextIdx]) otpEls[nextIdx].focus();
|
|
122
94
|
}
|
|
123
95
|
|
|
124
96
|
otpEls.forEach((el, i) => {
|
|
125
|
-
el.addEventListener(
|
|
126
|
-
el.addEventListener(
|
|
127
|
-
const v = e.target.value.replace(/\D/g,
|
|
97
|
+
el.addEventListener("paste", handleOtpPaste);
|
|
98
|
+
el.addEventListener("input", (e) => {
|
|
99
|
+
const v = e.target.value.replace(/\D/g, "").slice(0, 1);
|
|
128
100
|
e.target.value = v;
|
|
129
101
|
if (v && i < 5) otpEls[i + 1].focus();
|
|
130
102
|
});
|
|
131
|
-
el.addEventListener(
|
|
132
|
-
if (e.key ===
|
|
103
|
+
el.addEventListener("keydown", (e) => {
|
|
104
|
+
if (e.key === "Backspace" && !e.target.value && i > 0) otpEls[i - 1].focus();
|
|
133
105
|
});
|
|
134
106
|
});
|
|
135
107
|
|
|
136
|
-
form.addEventListener(
|
|
108
|
+
form.addEventListener("submit", (e) => {
|
|
137
109
|
e.preventDefault();
|
|
138
|
-
const code = [...otpEls].map((o) => o.value).join(
|
|
110
|
+
const code = [...otpEls].map((o) => o.value).join("");
|
|
139
111
|
if (code.length === 6 && firstName.value.trim() && lastName.value.trim()) {
|
|
140
112
|
onNext({
|
|
141
113
|
otp: code,
|
|
142
114
|
firstName: firstName.value.trim(),
|
|
143
115
|
lastName: lastName.value.trim(),
|
|
144
|
-
team:
|
|
145
|
-
subTeam:
|
|
146
|
-
designation:
|
|
116
|
+
team: orgLogic.getTeam(),
|
|
117
|
+
subTeam: orgLogic.getSubTeam(),
|
|
118
|
+
designation: orgLogic.getDesignation(),
|
|
147
119
|
});
|
|
148
120
|
}
|
|
149
121
|
});
|
|
150
122
|
|
|
151
|
-
wrap.querySelector('[data-action="back"]').addEventListener(
|
|
123
|
+
wrap.querySelector('[data-action="back"]').addEventListener("click", onBack);
|
|
152
124
|
|
|
153
125
|
return wrap;
|
|
154
126
|
}
|
package/ui/screens/index.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* Callbacks are async API handlers (called from index.js)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { STEPS } from
|
|
7
|
-
import { createEmailEntryScreen } from
|
|
8
|
-
import { createEmailVerifyScreen } from
|
|
9
|
-
import { createMobileEntryScreen } from
|
|
10
|
-
import { createMobileVerifyScreen } from
|
|
11
|
-
import { createPasswordSetupScreen } from
|
|
12
|
-
import { createOrgDetailsScreen } from
|
|
13
|
-
import { createFaceRegistrationScreen } from
|
|
14
|
-
import { createSuccessScreen } from
|
|
6
|
+
import { STEPS } from "../../core/flow.js";
|
|
7
|
+
import { createEmailEntryScreen } from "./email-entry.js";
|
|
8
|
+
import { createEmailVerifyScreen } from "./email-verify.js";
|
|
9
|
+
import { createMobileEntryScreen } from "./mobile-entry.js";
|
|
10
|
+
import { createMobileVerifyScreen } from "./mobile-verify.js";
|
|
11
|
+
import { createPasswordSetupScreen } from "./password-setup.js";
|
|
12
|
+
import { createOrgDetailsScreen } from "./org-details.js";
|
|
13
|
+
import { createFaceRegistrationScreen } from "./face-registration.js";
|
|
14
|
+
import { createSuccessScreen } from "./success.js";
|
|
15
15
|
|
|
16
16
|
export function createScreen(step, state, flow, callbacks) {
|
|
17
17
|
const {
|
|
@@ -31,41 +31,54 @@ export function createScreen(step, state, flow, callbacks) {
|
|
|
31
31
|
api,
|
|
32
32
|
} = callbacks;
|
|
33
33
|
|
|
34
|
+
const email = state.get("email") || "";
|
|
35
|
+
const derivedDomain = email.includes("@") ? email.split("@")[1] : null;
|
|
36
|
+
const entityDomain = state.get("entityDomain") || derivedDomain;
|
|
37
|
+
const entityName = state.get("entityName") || null;
|
|
38
|
+
const entityId = state.get("entityId") || null;
|
|
39
|
+
const orgStructure = state.get("orgStructure") || null;
|
|
40
|
+
|
|
34
41
|
switch (step) {
|
|
35
42
|
case STEPS.EMAIL:
|
|
36
43
|
return createEmailEntryScreen(onEmailSubmit, onSocialSignUp, onSwitchToLogin, onBackToApp);
|
|
37
44
|
|
|
38
45
|
case STEPS.EMAIL_VERIFY:
|
|
39
46
|
return createEmailVerifyScreen(
|
|
40
|
-
|
|
47
|
+
email,
|
|
41
48
|
(data) => onEmailVerify(data),
|
|
42
49
|
onBack,
|
|
43
50
|
{
|
|
44
|
-
firstName: state.get(
|
|
45
|
-
lastName: state.get(
|
|
46
|
-
team: state.get(
|
|
47
|
-
subTeam: state.get(
|
|
48
|
-
designation: state.get(
|
|
51
|
+
firstName: state.get("firstName") || "",
|
|
52
|
+
lastName: state.get("lastName") || "",
|
|
53
|
+
team: state.get("team") || "",
|
|
54
|
+
subTeam: state.get("subTeam") || "",
|
|
55
|
+
designation: state.get("designation") || "",
|
|
49
56
|
},
|
|
50
57
|
{
|
|
51
|
-
collectOrgDetails: state.get(
|
|
52
|
-
showOrgDetails: state.get(
|
|
58
|
+
collectOrgDetails: state.get("collectOrgDetails"),
|
|
59
|
+
showOrgDetails: state.get("showOrgDetails"),
|
|
60
|
+
api,
|
|
61
|
+
entityId,
|
|
62
|
+
entityName,
|
|
63
|
+
entityDomain,
|
|
64
|
+
orgStructure,
|
|
65
|
+
onOrgLoaded: (data) => state.set("orgStructure", data),
|
|
53
66
|
}
|
|
54
67
|
);
|
|
55
68
|
|
|
56
69
|
case STEPS.MOBILE:
|
|
57
70
|
return createMobileEntryScreen(onMobileSubmit, onBack, {
|
|
58
|
-
skipMobileOtp: state.get(
|
|
59
|
-
mobileVerification: state.get(
|
|
71
|
+
skipMobileOtp: state.get("skipMobileOtp"),
|
|
72
|
+
mobileVerification: state.get("mobileVerification"),
|
|
60
73
|
});
|
|
61
74
|
|
|
62
75
|
case STEPS.MOBILE_VERIFY:
|
|
63
76
|
return createMobileVerifyScreen(
|
|
64
|
-
state.get(
|
|
77
|
+
state.get("mobile"),
|
|
65
78
|
(otp, channel) => onMobileVerify(otp, channel),
|
|
66
79
|
onBack,
|
|
67
80
|
{
|
|
68
|
-
channel: state.get(
|
|
81
|
+
channel: state.get("mobileChannel") || "sms",
|
|
69
82
|
onResend: onMobileResend,
|
|
70
83
|
}
|
|
71
84
|
);
|
|
@@ -75,13 +88,19 @@ export function createScreen(step, state, flow, callbacks) {
|
|
|
75
88
|
(data) => onOrgDetailsSubmit(data),
|
|
76
89
|
onBack,
|
|
77
90
|
{
|
|
78
|
-
team: state.get(
|
|
79
|
-
subTeam: state.get(
|
|
80
|
-
designation: state.get(
|
|
91
|
+
team: state.get("team") || "",
|
|
92
|
+
subTeam: state.get("subTeam") || "",
|
|
93
|
+
designation: state.get("designation") || "",
|
|
81
94
|
},
|
|
82
95
|
{
|
|
83
|
-
collectOrgDetails: state.get(
|
|
84
|
-
showOrgDetails: state.get(
|
|
96
|
+
collectOrgDetails: state.get("collectOrgDetails"),
|
|
97
|
+
showOrgDetails: state.get("showOrgDetails"),
|
|
98
|
+
api,
|
|
99
|
+
entityId,
|
|
100
|
+
entityName,
|
|
101
|
+
entityDomain,
|
|
102
|
+
orgStructure,
|
|
103
|
+
onOrgLoaded: (data) => state.set("orgStructure", data),
|
|
85
104
|
}
|
|
86
105
|
);
|
|
87
106
|
|
|
@@ -96,9 +115,9 @@ export function createScreen(step, state, flow, callbacks) {
|
|
|
96
115
|
);
|
|
97
116
|
|
|
98
117
|
case STEPS.SUCCESS:
|
|
99
|
-
return createSuccessScreen(typeof onContinue ===
|
|
118
|
+
return createSuccessScreen(typeof onContinue === "function" ? onContinue : () => {});
|
|
100
119
|
|
|
101
120
|
default:
|
|
102
|
-
return document.createElement(
|
|
121
|
+
return document.createElement("div");
|
|
103
122
|
}
|
|
104
123
|
}
|
|
@@ -1,59 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Organization Details Screen — Team, Sub-team & Designation
|
|
2
|
+
* Organization Details Screen — Team, Sub-team & Designation Dropdowns
|
|
3
3
|
* v2.0 — warm, modern design for onboarding & social signup
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
6
|
+
import { renderOrgFieldsHtml, attachOrgFieldsLogic } from "./org-fields.js";
|
|
7
|
+
|
|
8
|
+
const BRIEFCASE_ICON = "M20 7h-4V4c0-1.1-.9-2-2-2h-4c-1.1 0-2 .9-2 2v3H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zM10 4h4v3h-4V4zm10 16H4V9h16v11z";
|
|
9
|
+
const ARROW_LEFT = "M19 12H5m7 7l-7-7 7-7";
|
|
10
|
+
const ARROW_RIGHT = "M5 12h14m-7-7l7 7-7 7";
|
|
9
11
|
|
|
10
12
|
export function createOrgDetailsScreen(onNext, onBack, prefill = {}, options = {}) {
|
|
11
|
-
const wrap = document.createElement(
|
|
12
|
-
wrap.className =
|
|
13
|
+
const wrap = document.createElement("div");
|
|
14
|
+
wrap.className = "wf-screen";
|
|
13
15
|
|
|
14
16
|
const orgOpt = options.collectOrgDetails !== undefined ? options.collectOrgDetails : options.showOrgDetails;
|
|
15
|
-
const showTeam = typeof orgOpt ===
|
|
16
|
-
const showSubTeam = typeof orgOpt ===
|
|
17
|
-
const showDesignation = typeof orgOpt ===
|
|
18
|
-
|
|
19
|
-
let fieldsHtml = '';
|
|
20
|
-
if (showTeam && showSubTeam) {
|
|
21
|
-
fieldsHtml += `
|
|
22
|
-
<div class="wf-row-2">
|
|
23
|
-
<div class="wf-field">
|
|
24
|
-
<label class="wf-label" for="wf-org-team">Team</label>
|
|
25
|
-
<input type="text" id="wf-org-team" placeholder="e.g. Engineering" class="wf-input" autocomplete="organization" />
|
|
26
|
-
</div>
|
|
27
|
-
<div class="wf-field">
|
|
28
|
-
<label class="wf-label" for="wf-org-subteam">Sub-team</label>
|
|
29
|
-
<input type="text" id="wf-org-subteam" placeholder="e.g. Backend" class="wf-input" />
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
`;
|
|
33
|
-
} else if (showTeam) {
|
|
34
|
-
fieldsHtml += `
|
|
35
|
-
<div class="wf-field">
|
|
36
|
-
<label class="wf-label" for="wf-org-team">Team</label>
|
|
37
|
-
<input type="text" id="wf-org-team" placeholder="e.g. Engineering" class="wf-input" autocomplete="organization" />
|
|
38
|
-
</div>
|
|
39
|
-
`;
|
|
40
|
-
} else if (showSubTeam) {
|
|
41
|
-
fieldsHtml += `
|
|
42
|
-
<div class="wf-field">
|
|
43
|
-
<label class="wf-label" for="wf-org-subteam">Sub-team</label>
|
|
44
|
-
<input type="text" id="wf-org-subteam" placeholder="e.g. Backend" class="wf-input" />
|
|
45
|
-
</div>
|
|
46
|
-
`;
|
|
47
|
-
}
|
|
17
|
+
const showTeam = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.team) : orgOpt !== false;
|
|
18
|
+
const showSubTeam = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.subTeam) : orgOpt !== false;
|
|
19
|
+
const showDesignation = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.designation) : orgOpt !== false;
|
|
48
20
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`;
|
|
56
|
-
}
|
|
21
|
+
const fieldsHtml = renderOrgFieldsHtml({
|
|
22
|
+
showTeam,
|
|
23
|
+
showSubTeam,
|
|
24
|
+
showDesignation,
|
|
25
|
+
prefix: "wf-org-",
|
|
26
|
+
});
|
|
57
27
|
|
|
58
28
|
wrap.innerHTML = `
|
|
59
29
|
<div class="wf-screen-header">
|
|
@@ -61,7 +31,7 @@ export function createOrgDetailsScreen(onNext, onBack, prefill = {}, options = {
|
|
|
61
31
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${BRIEFCASE_ICON}"/></svg>
|
|
62
32
|
</div>
|
|
63
33
|
<h2 class="wf-screen-title">Complete your profile</h2>
|
|
64
|
-
<p class="wf-screen-desc">
|
|
34
|
+
<p class="wf-screen-desc">Select your team and role to personalize your workspace</p>
|
|
65
35
|
</div>
|
|
66
36
|
<form class="wf-form" id="wf-org-form">
|
|
67
37
|
${fieldsHtml}
|
|
@@ -77,27 +47,28 @@ export function createOrgDetailsScreen(onNext, onBack, prefill = {}, options = {
|
|
|
77
47
|
</form>
|
|
78
48
|
`;
|
|
79
49
|
|
|
80
|
-
const form = wrap.querySelector(
|
|
81
|
-
const teamInput = wrap.querySelector('#wf-org-team');
|
|
82
|
-
const subTeamInput = wrap.querySelector('#wf-org-subteam');
|
|
83
|
-
const designationInput = wrap.querySelector('#wf-org-designation');
|
|
50
|
+
const form = wrap.querySelector("#wf-org-form");
|
|
84
51
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
52
|
+
// Initialize and attach dynamic org dropdowns logic
|
|
53
|
+
const orgLogic = attachOrgFieldsLogic({
|
|
54
|
+
wrap,
|
|
55
|
+
prefix: "wf-org-",
|
|
56
|
+
prefill,
|
|
57
|
+
options,
|
|
58
|
+
});
|
|
88
59
|
|
|
89
|
-
form.addEventListener(
|
|
60
|
+
form.addEventListener("submit", (e) => {
|
|
90
61
|
e.preventDefault();
|
|
91
62
|
onNext({
|
|
92
|
-
team:
|
|
93
|
-
subTeam:
|
|
94
|
-
designation:
|
|
63
|
+
team: orgLogic.getTeam(),
|
|
64
|
+
subTeam: orgLogic.getSubTeam(),
|
|
65
|
+
designation: orgLogic.getDesignation(),
|
|
95
66
|
});
|
|
96
67
|
});
|
|
97
68
|
|
|
98
69
|
const backBtn = wrap.querySelector('button[data-action="back"]');
|
|
99
|
-
if (backBtn && typeof onBack ===
|
|
100
|
-
backBtn.addEventListener(
|
|
70
|
+
if (backBtn && typeof onBack === "function") {
|
|
71
|
+
backBtn.addEventListener("click", onBack);
|
|
101
72
|
}
|
|
102
73
|
|
|
103
74
|
return wrap;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Org Fields (Team, Sub-team, Designation) Dropdowns Component
|
|
3
|
+
* Handles dynamic fetching from /org/structure and cascaded sub-team filtering.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function escapeHtml(str) {
|
|
7
|
+
if (!str) return "";
|
|
8
|
+
return String(str)
|
|
9
|
+
.replace(/&/g, "&")
|
|
10
|
+
.replace(/</g, "<")
|
|
11
|
+
.replace(/>/g, ">")
|
|
12
|
+
.replace(/"/g, """);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderOrgFieldsHtml({
|
|
16
|
+
showTeam = true,
|
|
17
|
+
showSubTeam = true,
|
|
18
|
+
showDesignation = true,
|
|
19
|
+
prefix = "wf-",
|
|
20
|
+
} = {}) {
|
|
21
|
+
let html = "";
|
|
22
|
+
|
|
23
|
+
if (showTeam && showSubTeam) {
|
|
24
|
+
html += `
|
|
25
|
+
<div class="wf-row-2">
|
|
26
|
+
<div class="wf-field">
|
|
27
|
+
<label class="wf-label" for="${prefix}team">Team</label>
|
|
28
|
+
<select id="${prefix}team" class="wf-input wf-select" autocomplete="organization">
|
|
29
|
+
<option value="">Select Team</option>
|
|
30
|
+
</select>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="wf-field">
|
|
33
|
+
<label class="wf-label" for="${prefix}sub-team">Sub-team</label>
|
|
34
|
+
<select id="${prefix}sub-team" class="wf-input wf-select" disabled>
|
|
35
|
+
<option value="">Select a team first</option>
|
|
36
|
+
</select>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
`;
|
|
40
|
+
} else if (showTeam) {
|
|
41
|
+
html += `
|
|
42
|
+
<div class="wf-field">
|
|
43
|
+
<label class="wf-label" for="${prefix}team">Team</label>
|
|
44
|
+
<select id="${prefix}team" class="wf-input wf-select" autocomplete="organization">
|
|
45
|
+
<option value="">Select Team</option>
|
|
46
|
+
</select>
|
|
47
|
+
</div>
|
|
48
|
+
`;
|
|
49
|
+
} else if (showSubTeam) {
|
|
50
|
+
html += `
|
|
51
|
+
<div class="wf-field">
|
|
52
|
+
<label class="wf-label" for="${prefix}sub-team">Sub-team</label>
|
|
53
|
+
<select id="${prefix}sub-team" class="wf-input wf-select" disabled>
|
|
54
|
+
<option value="">Select a team first</option>
|
|
55
|
+
</select>
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (showDesignation) {
|
|
61
|
+
html += `
|
|
62
|
+
<div class="wf-field">
|
|
63
|
+
<label class="wf-label" for="${prefix}designation">Designation</label>
|
|
64
|
+
<select id="${prefix}designation" class="wf-input wf-select" autocomplete="organization-title">
|
|
65
|
+
<option value="">Select Designation</option>
|
|
66
|
+
</select>
|
|
67
|
+
</div>
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return html;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function attachOrgFieldsLogic({
|
|
75
|
+
wrap,
|
|
76
|
+
prefix = "wf-",
|
|
77
|
+
prefill = {},
|
|
78
|
+
options = {},
|
|
79
|
+
}) {
|
|
80
|
+
const teamEl = wrap.querySelector(`#${prefix}team`);
|
|
81
|
+
const subTeamEl = wrap.querySelector(`#${prefix}sub-team`);
|
|
82
|
+
const designationEl = wrap.querySelector(`#${prefix}designation`);
|
|
83
|
+
|
|
84
|
+
let currentTeams = [];
|
|
85
|
+
let currentDesignations = [];
|
|
86
|
+
|
|
87
|
+
function updateSubTeams(selectedTeamName, selectedSubTeamValue = "") {
|
|
88
|
+
if (!subTeamEl) return;
|
|
89
|
+
if (!selectedTeamName) {
|
|
90
|
+
subTeamEl.innerHTML = '<option value="">Select a team first</option>';
|
|
91
|
+
subTeamEl.disabled = true;
|
|
92
|
+
subTeamEl.value = "";
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const team = currentTeams.find((t) => t.name === selectedTeamName || t.id === selectedTeamName);
|
|
97
|
+
const subTeams = team && Array.isArray(team.sub_teams) ? team.sub_teams : [];
|
|
98
|
+
|
|
99
|
+
if (subTeams.length === 0) {
|
|
100
|
+
subTeamEl.innerHTML = '<option value="">No sub-teams</option>';
|
|
101
|
+
subTeamEl.disabled = true;
|
|
102
|
+
subTeamEl.value = "";
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let opts = '<option value="">Select Sub-team</option>';
|
|
107
|
+
subTeams.forEach((st) => {
|
|
108
|
+
opts += `<option value="${escapeHtml(st.name)}">${escapeHtml(st.name)}</option>`;
|
|
109
|
+
});
|
|
110
|
+
subTeamEl.innerHTML = opts;
|
|
111
|
+
subTeamEl.disabled = false;
|
|
112
|
+
|
|
113
|
+
if (selectedSubTeamValue) {
|
|
114
|
+
subTeamEl.value = selectedSubTeamValue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function populate(orgData) {
|
|
119
|
+
if (!orgData) return;
|
|
120
|
+
currentTeams = Array.isArray(orgData.teams) ? orgData.teams : [];
|
|
121
|
+
currentDesignations = Array.isArray(orgData.designations) ? orgData.designations : [];
|
|
122
|
+
|
|
123
|
+
// Populate Teams
|
|
124
|
+
if (teamEl) {
|
|
125
|
+
const prevVal = teamEl.value || prefill.team || "";
|
|
126
|
+
let opts = '<option value="">Select Team</option>';
|
|
127
|
+
currentTeams.forEach((t) => {
|
|
128
|
+
opts += `<option value="${escapeHtml(t.name)}">${escapeHtml(t.name)}</option>`;
|
|
129
|
+
});
|
|
130
|
+
teamEl.innerHTML = opts;
|
|
131
|
+
if (prevVal) {
|
|
132
|
+
teamEl.value = prevVal;
|
|
133
|
+
updateSubTeams(prevVal, subTeamEl?.value || prefill.subTeam || "");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Populate Designations
|
|
138
|
+
if (designationEl) {
|
|
139
|
+
const prevVal = designationEl.value || prefill.designation || "";
|
|
140
|
+
let opts = '<option value="">Select Designation</option>';
|
|
141
|
+
currentDesignations.forEach((d) => {
|
|
142
|
+
opts += `<option value="${escapeHtml(d.name)}">${escapeHtml(d.name)}</option>`;
|
|
143
|
+
});
|
|
144
|
+
designationEl.innerHTML = opts;
|
|
145
|
+
if (prevVal) {
|
|
146
|
+
designationEl.value = prevVal;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Handle Team change -> cascade Sub-teams
|
|
152
|
+
if (teamEl) {
|
|
153
|
+
teamEl.addEventListener("change", () => {
|
|
154
|
+
updateSubTeams(teamEl.value);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If org structure was already loaded, populate immediately
|
|
159
|
+
if (options.orgStructure) {
|
|
160
|
+
populate(options.orgStructure);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// If api is available, ensure we have the latest org structure
|
|
164
|
+
if (options.api && typeof options.api.getOrgStructure === "function") {
|
|
165
|
+
const { entityId, entityName, entityDomain, onOrgLoaded } = options;
|
|
166
|
+
if (entityId || entityName || entityDomain) {
|
|
167
|
+
options.api.getOrgStructure({ entityId, entityName, entityDomain })
|
|
168
|
+
.then((res) => {
|
|
169
|
+
if (res && (Array.isArray(res.teams) || Array.isArray(res.designations))) {
|
|
170
|
+
if (typeof onOrgLoaded === "function") onOrgLoaded(res);
|
|
171
|
+
populate(res);
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
.catch(() => {});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
getTeam() {
|
|
180
|
+
return teamEl ? teamEl.value.trim() : "";
|
|
181
|
+
},
|
|
182
|
+
getSubTeam() {
|
|
183
|
+
return subTeamEl && !subTeamEl.disabled ? subTeamEl.value.trim() : "";
|
|
184
|
+
},
|
|
185
|
+
getDesignation() {
|
|
186
|
+
return designationEl ? designationEl.value.trim() : "";
|
|
187
|
+
},
|
|
188
|
+
populate,
|
|
189
|
+
};
|
|
190
|
+
}
|