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.
@@ -354,9 +354,13 @@ const instance = startSignup({
354
354
 
355
355
  ## 5.3. Organization details (Team, Sub-team & Designation)
356
356
 
357
- During the email verification step, the SDK by default renders fields to collect **Team**, **Sub-team**, and **Designation** alongside First and Last Name.
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
- 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`**).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "central-auth-js-sdk",
3
- "version": "1.2.10",
3
+ "version": "1.2.11",
4
4
  "description": "Vanilla JS sign-up and login flow SDK — embeddable auth UI with configurable methods",
5
5
  "type": "module",
6
6
  "main": "index.js",
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
- 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';
7
- const ARROW_LEFT = 'M19 12H5m7 7l-7-7 7-7';
8
- const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
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('div');
12
- wrap.className = 'wf-screen';
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 === 'object' && orgOpt !== null ? Boolean(orgOpt.team) : orgOpt !== false;
16
- const showSubTeam = typeof orgOpt === 'object' && orgOpt !== null ? Boolean(orgOpt.subTeam) : orgOpt !== false;
17
- const showDesignation = typeof orgOpt === 'object' && orgOpt !== null ? Boolean(orgOpt.designation) : orgOpt !== false;
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
- let orgFieldsHtml = '';
20
- if (showTeam && showSubTeam) {
21
- orgFieldsHtml += `
22
- <div class="wf-row-2">
23
- <div class="wf-field">
24
- <label class="wf-label" for="wf-team">Team</label>
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('#wf-email-verify-form');
102
- const otpEls = wrap.querySelectorAll('.wf-otp-input');
103
- const firstName = wrap.querySelector('#wf-first-name');
104
- const lastName = wrap.querySelector('#wf-last-name');
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
- if (prefill.team && team) team.value = prefill.team;
112
- if (prefill.subTeam && subTeam) subTeam.value = prefill.subTeam;
113
- if (prefill.designation && designation) designation.value = prefill.designation;
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('text');
118
- const digits = pasted.replace(/\D/g, '').slice(0, 6).split('');
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('paste', handleOtpPaste);
126
- el.addEventListener('input', (e) => {
127
- const v = e.target.value.replace(/\D/g, '').slice(0, 1);
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('keydown', (e) => {
132
- if (e.key === 'Backspace' && !e.target.value && i > 0) otpEls[i - 1].focus();
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('submit', (e) => {
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: team ? team.value.trim() : '',
145
- subTeam: subTeam ? subTeam.value.trim() : '',
146
- designation: designation ? designation.value.trim() : '',
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('click', onBack);
123
+ wrap.querySelector('[data-action="back"]').addEventListener("click", onBack);
152
124
 
153
125
  return wrap;
154
126
  }
@@ -3,15 +3,15 @@
3
3
  * Callbacks are async API handlers (called from index.js)
4
4
  */
5
5
 
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';
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
- state.get('email'),
47
+ email,
41
48
  (data) => onEmailVerify(data),
42
49
  onBack,
43
50
  {
44
- firstName: state.get('firstName') || '',
45
- lastName: state.get('lastName') || '',
46
- team: state.get('team') || '',
47
- subTeam: state.get('subTeam') || '',
48
- designation: state.get('designation') || '',
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('collectOrgDetails'),
52
- showOrgDetails: state.get('showOrgDetails'),
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('skipMobileOtp'),
59
- mobileVerification: state.get('mobileVerification'),
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('mobile'),
77
+ state.get("mobile"),
65
78
  (otp, channel) => onMobileVerify(otp, channel),
66
79
  onBack,
67
80
  {
68
- channel: state.get('mobileChannel') || 'sms',
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('team') || '',
79
- subTeam: state.get('subTeam') || '',
80
- designation: state.get('designation') || '',
91
+ team: state.get("team") || "",
92
+ subTeam: state.get("subTeam") || "",
93
+ designation: state.get("designation") || "",
81
94
  },
82
95
  {
83
- collectOrgDetails: state.get('collectOrgDetails'),
84
- showOrgDetails: state.get('showOrgDetails'),
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 === 'function' ? onContinue : () => {});
118
+ return createSuccessScreen(typeof onContinue === "function" ? onContinue : () => {});
100
119
 
101
120
  default:
102
- return document.createElement('div');
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
- 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';
7
- const ARROW_LEFT = 'M19 12H5m7 7l-7-7 7-7';
8
- const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
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('div');
12
- wrap.className = 'wf-screen';
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 === 'object' && orgOpt !== null ? Boolean(orgOpt.team) : orgOpt !== false;
16
- const showSubTeam = typeof orgOpt === 'object' && orgOpt !== null ? Boolean(orgOpt.subTeam) : orgOpt !== false;
17
- const showDesignation = typeof orgOpt === 'object' && orgOpt !== null ? Boolean(orgOpt.designation) : orgOpt !== false;
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
- if (showDesignation) {
50
- fieldsHtml += `
51
- <div class="wf-field">
52
- <label class="wf-label" for="wf-org-designation">Designation</label>
53
- <input type="text" id="wf-org-designation" placeholder="e.g. Software Engineer" class="wf-input" autocomplete="organization-title" />
54
- </div>
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">Add your team and role details to personalize your workspace</p>
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('#wf-org-form');
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
- if (prefill.team && teamInput) teamInput.value = prefill.team;
86
- if (prefill.subTeam && subTeamInput) subTeamInput.value = prefill.subTeam;
87
- if (prefill.designation && designationInput) designationInput.value = prefill.designation;
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('submit', (e) => {
60
+ form.addEventListener("submit", (e) => {
90
61
  e.preventDefault();
91
62
  onNext({
92
- team: teamInput ? teamInput.value.trim() : '',
93
- subTeam: subTeamInput ? subTeamInput.value.trim() : '',
94
- designation: designationInput ? designationInput.value.trim() : '',
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 === 'function') {
100
- backBtn.addEventListener('click', onBack);
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, "&amp;")
10
+ .replace(/</g, "&lt;")
11
+ .replace(/>/g, "&gt;")
12
+ .replace(/"/g, "&quot;");
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
+ }