@things-factory/auth-ui 10.1.215 → 10.1.219

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.
Files changed (59) hide show
  1. package/dist-client/bootstrap.js +18 -14
  2. package/dist-client/bootstrap.js.map +1 -1
  3. package/dist-client/components/abstract-password-reset.d.ts +5 -0
  4. package/dist-client/components/abstract-password-reset.js +20 -0
  5. package/dist-client/components/abstract-password-reset.js.map +1 -1
  6. package/dist-client/components/ownership-transfer-popup.d.ts +3 -1
  7. package/dist-client/components/ownership-transfer-popup.js +38 -41
  8. package/dist-client/components/ownership-transfer-popup.js.map +1 -1
  9. package/dist-client/components/password-setup-link-popup.d.ts +46 -0
  10. package/dist-client/components/password-setup-link-popup.js +254 -0
  11. package/dist-client/components/password-setup-link-popup.js.map +1 -0
  12. package/dist-client/components/user-detail.d.ts +20 -30
  13. package/dist-client/components/user-detail.js +114 -186
  14. package/dist-client/components/user-detail.js.map +1 -1
  15. package/dist-client/components/user-invite.d.ts +18 -25
  16. package/dist-client/components/user-invite.js +157 -181
  17. package/dist-client/components/user-invite.js.map +1 -1
  18. package/dist-client/components/user-popups-standard.test.js +69 -0
  19. package/dist-client/components/user-popups-standard.test.js.map +1 -0
  20. package/dist-client/components/user-role-editor.js +5 -26
  21. package/dist-client/components/user-role-editor.js.map +1 -1
  22. package/dist-client/components/user-status-renderer.js +4 -0
  23. package/dist-client/components/user-status-renderer.js.map +1 -1
  24. package/dist-client/entries/auth/reset-password.d.ts +1 -0
  25. package/dist-client/entries/auth/reset-password.js +5 -1
  26. package/dist-client/entries/auth/reset-password.js.map +1 -1
  27. package/dist-client/index.js +10 -28
  28. package/dist-client/index.js.map +1 -1
  29. package/dist-client/pages/user/user-list-page.d.ts +5 -1
  30. package/dist-client/pages/user/user-list-page.js +14 -5
  31. package/dist-client/pages/user/user-list-page.js.map +1 -1
  32. package/dist-client/route.js +0 -3
  33. package/dist-client/route.js.map +1 -1
  34. package/dist-client/tsconfig.tsbuildinfo +1 -1
  35. package/dist-client/utils/link-token.d.ts +3 -3
  36. package/dist-client/utils/link-token.js +4 -4
  37. package/dist-client/utils/link-token.js.map +1 -1
  38. package/dist-client/utils/link-token.test.js +2 -2
  39. package/dist-client/utils/link-token.test.js.map +1 -1
  40. package/package.json +6 -5
  41. package/things-factory.config.js +0 -1
  42. package/translations/en.json +30 -20
  43. package/translations/es.json +30 -20
  44. package/translations/ja.json +30 -20
  45. package/translations/ko.json +30 -20
  46. package/translations/ms.json +30 -20
  47. package/translations/zh.json +30 -20
  48. package/dist-client/components/accept-invitation.d.ts +0 -47
  49. package/dist-client/components/accept-invitation.js +0 -258
  50. package/dist-client/components/accept-invitation.js.map +0 -1
  51. package/dist-client/entries/auth/accept-invitation.d.ts +0 -3
  52. package/dist-client/entries/auth/accept-invitation.js +0 -10
  53. package/dist-client/entries/auth/accept-invitation.js.map +0 -1
  54. package/dist-client/pages/invitation/invitation-list-query.test.js +0 -69
  55. package/dist-client/pages/invitation/invitation-list-query.test.js.map +0 -1
  56. package/dist-client/pages/invitation/invitations.d.ts +0 -63
  57. package/dist-client/pages/invitation/invitations.js +0 -284
  58. package/dist-client/pages/invitation/invitations.js.map +0 -1
  59. /package/dist-client/{pages/invitation/invitation-list-query.test.d.ts → components/user-popups-standard.test.d.ts} +0 -0
@@ -1,15 +1,14 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
- import '@material/web/icon/icon.js';
3
- import '@material/web/button/elevated-button.js';
4
- import '@material/web/textfield/filled-text-field.js';
5
- import '@material/web/checkbox/checkbox.js';
2
+ import '@material/web/button/text-button.js';
3
+ import '@material/web/button/filled-button.js';
4
+ import '@operato/popup/ox-popup-actions.js';
6
5
  import gql from 'graphql-tag';
7
6
  import { css, html, LitElement, nothing } from 'lit';
8
7
  import { customElement, query, state } from 'lit/decorators.js';
9
8
  import { client, gqlContext } from '@operato/graphql';
10
9
  import { i18next, localize } from '@operato/i18n';
11
10
  import { InheritedValueType } from '@operato/shell';
12
- import { ButtonContainerStyles, FormFieldStyles } from '@operato/styles';
11
+ import { CommonPopupStyles, FormFieldStyles, PopupBodyStyles, PopupFieldStyles } from '@operato/styles';
13
12
  /**
14
13
  * 사람을 이 도메인으로 부르는 자리.
15
14
  *
@@ -23,16 +22,12 @@ import { ButtonContainerStyles, FormFieldStyles } from '@operato/styles';
23
22
  * 자리는 기본 비밀번호가 설정된 도메인에서만 나타났다.
24
23
  *
25
24
  * ── 그래서 이메일 하나로 시작해 갈라 보여 준다 ──────────────────────────────
26
- * `checkUserExistence` 가 시스템 전체에서 그 이메일을 찾는다. 관리자는 「초대인가 생성인가」를
25
+ * `checkUserExistence` 가 시스템 전체에서 그 이메일을 찾는다. 관리자는 「넣기인가 만들기인가」를
27
26
  * 먼저 고르지 않는다 — 이메일을 넣으면 화면이 무엇이 일어날지 말하고, 단추 글자도 그에 맞춘다.
28
27
  *
29
- * 계정이 있다 이 도메인에 바로 들어온다 (`inviteUser`)
30
- * 없고 기본 비밀번호 있다 계정을 만들어 넣는다 (`createUser`) — 비밀번호를 관리자가 전한다
31
- * 없고 기본 비밀번호 없다 **길이 없다.** 단추를 내주지 않고 무엇이 막고 있는지 말한다
32
- *
33
- * ⚠ **셋 중 어느 것도 「초대장」이 아니다.** 상대가 수락하는 흐름은 서버에 아직 없다
34
- * (`sendInvitation` 도 가입한 사용자만 받고, `Invitation` 에 상태도 만료도 없다). 그래서 이
35
- * 화면은 초대라는 말을 쓰지 않고 **실제로 일어나는 일**을 쓴다.
28
+ * 계정이 있다 이 도메인에 바로 들어온다 (`inviteUser`)
29
+ * 계정이 없다 「비밀번호 설정 대기」 계정을 만들고(`createUser`), 이어서 비밀번호 설정 링크를
30
+ * 건넨다(`password-setup-link-popup`). 관리자는 비밀번호를 정하지 않는다(ADR-0161).
36
31
  *
37
32
  * ── 역할을 함께 고른다 ──────────────────────────────────────────────────────
38
33
  * 역할 없이 들어온 사람은 로그인해도 화면이 하나도 없다. 부르는 자리에서 함께 정하면 그 사람이
@@ -52,57 +47,65 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
52
47
  this.selected = new Set();
53
48
  this.working = false;
54
49
  this.message = '';
55
- /** 기본 비밀번호가 설정돼 있지 않으면 계정을 만드는 길이 없다 — 서버가 던진다. */
56
- this.defaultPasswordConfigured = false;
57
50
  }
51
+ /* 팝업의 집 모양(page-standard §7·§8): 본문만 스크롤, 칸은 라벨 위의 날것 입력, 단추 줄은 `ox-popup-actions`. */
58
52
  static { this.styles = [
53
+ CommonPopupStyles,
54
+ PopupBodyStyles,
59
55
  FormFieldStyles,
60
- ButtonContainerStyles,
56
+ PopupFieldStyles,
61
57
  css `
62
- :host {
63
- display: flex;
64
- flex-direction: column;
65
- gap: var(--spacing-medium, 8px);
66
- padding: var(--spacing-large, 12px);
67
- min-width: min(420px, 86vw);
68
- background-color: var(--md-sys-color-surface);
69
- color: var(--md-sys-color-on-surface);
58
+ /* 오류 문장은 칸 바로 아래(§7). */
59
+ [invalid] {
60
+ color: var(--ui-crit, var(--md-sys-color-error));
61
+ font-size: var(--ui-t-small, 11.5px);
70
62
  }
71
63
 
72
- label {
73
- color: var(--md-sys-color-on-surface-variant);
74
- font-size: 0.8rem;
64
+ [invalid]:empty {
65
+ display: none;
75
66
  }
76
67
 
77
- input {
78
- width: 100%;
68
+ [guide] p {
69
+ margin: 0 0 var(--spacing-small, 4px);
70
+ line-height: var(--popup-line-height);
79
71
  }
80
72
 
81
- [section] {
82
- margin-top: var(--spacing-medium, 8px);
83
- color: var(--md-sys-color-on-surface-variant);
84
- font-size: 0.8rem;
73
+ [guide] dl {
74
+ display: grid;
75
+ grid-template-columns: max-content 1fr;
76
+ gap: 2px var(--spacing-large, 12px);
77
+ margin: 0;
78
+ color: var(--ui-muted);
79
+ font-size: var(--ui-t-small, 11.5px);
80
+ line-height: var(--popup-line-height);
85
81
  }
86
82
 
87
- /* 색은 짝으로. 막고 있는 것만 눈에 걸리게 한다. */
88
- [note] {
89
- padding: 8px 10px;
90
- border-radius: var(--md-sys-shape-corner-small, 6px);
91
- font-size: 0.82rem;
92
- line-height: 1.5;
93
- background-color: var(--md-sys-color-surface-variant);
94
- color: var(--md-sys-color-on-surface-variant);
83
+ [guide] dl > div {
84
+ display: contents;
95
85
  }
96
86
 
97
- [note][attention] {
98
- background-color: var(--md-sys-color-error-container);
99
- color: var(--md-sys-color-on-error-container);
87
+ [guide] dt {
88
+ font-weight: 600;
100
89
  }
101
90
 
102
- [invalid] {
103
- color: var(--md-sys-color-error);
104
- font-size: 0.8rem;
105
- min-height: 1.1rem;
91
+ [guide] dd {
92
+ margin: 0;
93
+ }
94
+
95
+ /* 넣은 이메일이 해당하는 갈래 — 띠로 짚는다. */
96
+ [guide] dl > div > * {
97
+ padding: 1px var(--spacing-small, 4px);
98
+ }
99
+
100
+ [guide] dl > div[current] > * {
101
+ background: var(--ui-line-soft, var(--md-sys-color-surface-container));
102
+ color: var(--ui-ink);
103
+ }
104
+
105
+ [group-label] {
106
+ margin-top: var(--spacing-medium, 8px);
107
+ color: var(--ui-muted);
108
+ font-size: var(--ui-t-small, 11.5px);
106
109
  }
107
110
 
108
111
  ul[roles] {
@@ -114,123 +117,119 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
114
117
  ul[roles] li {
115
118
  display: flex;
116
119
  align-items: center;
117
- gap: 8px;
118
- padding: 4px 2px;
120
+ gap: var(--spacing-medium, 8px);
121
+ padding: 3px 0;
119
122
  }
120
123
  `
121
124
  ]; }
122
125
  /** 계정을 새로 만드는 경로인가. */
123
126
  get creating() {
124
- return this.lookup === 'absent' && this.defaultPasswordConfigured;
125
- }
126
- /**
127
- * 메일로 초대하는 경로인가.
128
- *
129
- * 계정이 없고 기본 비밀번호도 없을 때다. 관리자가 비밀번호를 대신 정해 줄 수 없으므로
130
- * **본인이 메일을 받아 스스로 정하는 것이 유일한 길**이다.
131
- */
132
- get inviting() {
133
- return this.lookup === 'absent' && !this.defaultPasswordConfigured;
127
+ return this.lookup === 'absent';
134
128
  }
135
129
  /** 부를 수 있나 — 단추와 처리기가 같은 답을 본다. */
136
130
  get callable() {
137
- return !this.working && (this.lookup === 'found' || this.creating || this.inviting);
131
+ return !this.working && (this.lookup === 'found' || this.creating);
138
132
  }
139
133
  render() {
140
134
  return html `
141
- <md-filled-text-field
142
- id="email"
143
- name="email"
144
- type="email"
145
- label=${i18next.t('field.email')}
146
- .value=${this.email}
147
- placeholder="name@company.com"
148
- @input=${(e) => this.onEmail(e.target.value)}
149
- ></md-filled-text-field>
150
- <div invalid>${this.message}</div>
135
+ <div class="body">
136
+ ${this.renderGuide()}
151
137
 
152
- ${this.renderLookup()} ${this.creating ? this.renderNewAccount() : nothing}
153
- ${this.lookup === 'found' || this.creating || this.inviting ? this.renderRoles() : nothing}
138
+ <label class="field">
139
+ <span>${i18next.t('field.email')}</span>
140
+ <input
141
+ name="email"
142
+ type="email"
143
+ .value=${this.email}
144
+ placeholder="name@company.com"
145
+ @input=${(e) => this.onEmail(e.target.value)}
146
+ />
147
+ <span invalid>${this.message}</span>
148
+ </label>
154
149
 
155
- <div class="button-container">
156
- <md-elevated-button ?disabled=${!this.callable} @click=${() => this.onCall()}>
157
- ${this.inviting
158
- ? i18next.t('label.send invitation')
159
- : this.creating
160
- ? i18next.t('label.create account and add')
161
- : i18next.t('label.add to domain')}
162
- </md-elevated-button>
163
- <md-elevated-button @click=${() => this.dispatchEvent(new CustomEvent('close'))}>
164
- ${i18next.t('button.cancel')}
165
- </md-elevated-button>
150
+ ${this.lookup === 'checking' ? html `<div class="note">${i18next.t('text.loading')}</div>` : nothing}
151
+ ${this.creating ? this.renderNewAccount() : nothing}
152
+ ${this.lookup === 'found' || this.creating ? this.renderRoles() : nothing}
166
153
  </div>
154
+
155
+ <ox-popup-actions>
156
+ <md-text-button slot="cancel" @click=${() => this.dispatchEvent(new CustomEvent('close'))}>
157
+ ${i18next.t('button.cancel')}
158
+ </md-text-button>
159
+ <md-filled-button slot="confirm" ?disabled=${!this.callable} @click=${() => this.onCall()}>
160
+ ${this.creating ? i18next.t('label.create account and add') : i18next.t('label.add to domain')}
161
+ </md-filled-button>
162
+ </ox-popup-actions>
167
163
  `;
168
164
  }
169
- renderLookup() {
170
- switch (this.lookup) {
171
- case 'checking':
172
- return html `<div note>${i18next.t('text.loading')}</div>`;
173
- case 'found':
174
- return html `<div note>${i18next.t('text.account exists so joins immediately')}</div>`;
175
- case 'absent':
176
- return this.defaultPasswordConfigured
177
- ? html `<div note>${i18next.t('text.account will be created with the default password')}</div>`
178
- : /*
179
- * 전에는 여기가 막다른 자리였다 — 관리자가 비밀번호를 대신 정해 줄 수 없으니 계정을
180
- * 만들 길이 없었다. 이제 초대장이 그 길이다. 본인이 메일을 받아 스스로 비밀번호를
181
- * 정하고 들어온다.
182
- */
183
- html `<div note>${i18next.t('text.no account so an invitation will be mailed')}</div>`;
184
- default:
185
- return nothing;
186
- }
165
+ /**
166
+ * 사용자 추가가 무엇인지 — 이메일을 넣기 전에 말한다.
167
+ *
168
+ * 칸 하나만 있으면 이 팝업이 무엇을 하는지 알 수 없다. 계정은 하나로 여러 도메인에서 쓰고, 추가는
169
+ * 그 계정을 이 도메인에 넣는 일이다. 이메일에 따라 일어나는 일이 갈리므로 갈래를 먼저 보이고,
170
+ * 이메일을 찾으면 해당하는 줄을 짚는다.
171
+ */
172
+ renderGuide() {
173
+ return html `
174
+ <div guide>
175
+ <p>${i18next.t('text.add user puts an account into this domain')}</p>
176
+ <dl>
177
+ <div ?current=${this.lookup === 'found'}>
178
+ <dt>${i18next.t('label.account exists')}</dt>
179
+ <dd>${i18next.t('text.they join this domain at once')}</dd>
180
+ </div>
181
+ <div ?current=${this.lookup === 'absent'}>
182
+ <dt>${i18next.t('label.no account')}</dt>
183
+ <dd>${i18next.t('text.an account is made and a setup link handed over')}</dd>
184
+ </div>
185
+ <div>
186
+ <dt>${i18next.t('field.roles')}</dt>
187
+ <dd>${i18next.t('text.roles decide the screens they open')}</dd>
188
+ </div>
189
+ </dl>
190
+ </div>
191
+ `;
187
192
  }
188
193
  renderNewAccount() {
189
194
  return html `
190
- <md-filled-text-field
191
- id="new-name"
192
- name="name"
193
- type="text"
194
- label=${i18next.t('field.name')}
195
- @input=${() => this.requestUpdate()}
196
- ></md-filled-text-field>
195
+ <label class="field">
196
+ <span>${i18next.t('field.name')}</span>
197
+ <input name="name" type="text" @input=${() => this.requestUpdate()} />
198
+ </label>
197
199
 
198
200
  <!-- 서버가 받는 글자는 A-Z a-z 0-9 . _ @ - 다. 비우면 이메일을 그대로 쓴다. -->
199
- <md-filled-text-field
200
- id="new-username"
201
- name="username"
202
- type="text"
203
- label=${i18next.t('field.username')}
204
- placeholder=${this.email}
205
- ></md-filled-text-field>
201
+ <label class="field">
202
+ <span>${i18next.t('field.username')}</span>
203
+ <input name="username" type="text" placeholder=${this.email} />
204
+ </label>
206
205
  `;
207
206
  }
208
207
  renderRoles() {
209
208
  return html `
210
- <div section>${i18next.t('field.roles')}</div>
209
+ <div group-label>${i18next.t('field.roles')}</div>
211
210
  ${this.roles.length
212
211
  ? html `<ul roles>
213
212
  ${this.roles.map(role => html `
214
213
  <li>
215
- <md-checkbox
214
+ <input
215
+ type="checkbox"
216
216
  id="invite-role-${role.id}"
217
217
  .checked=${this.selected.has(role.id)}
218
218
  @change=${(e) => this.onToggle(role.id, e.target.checked)}
219
- ></md-checkbox>
219
+ />
220
220
  <label for="invite-role-${role.id}">${role.name}</label>
221
221
  </li>
222
222
  `)}
223
223
  </ul>`
224
- : html `<div note attention>${i18next.t('text.no role defined in this domain')}</div>`}
224
+ : html `<div class="failure">${i18next.t('text.no role defined in this domain')}</div>`}
225
225
  ${this.roles.length && this.selected.size === 0
226
- ? html `<div note attention>${i18next.t('text.without a role they see nothing')}</div>`
226
+ ? html `<div class="failure">${i18next.t('text.without a role they see nothing')}</div>`
227
227
  : nothing}
228
228
  `;
229
229
  }
230
230
  connectedCallback() {
231
231
  super.connectedCallback();
232
232
  this.fetchRoles();
233
- this.checkDefaultPassword();
234
233
  }
235
234
  disconnectedCallback() {
236
235
  super.disconnectedCallback();
@@ -250,56 +249,33 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
250
249
  async check() {
251
250
  const email = this.email.trim();
252
251
  this.lookup = 'checking';
253
- const response = await client.query({
254
- query: gql `
255
- query ($email: EmailAddress!) {
256
- checkUserExistence(email: $email)
257
- }
258
- `,
259
- variables: { email },
260
- context: gqlContext()
261
- });
262
- /* 묻는 사이에 사람이 더 쳤으면 그 답은 이미 옛것이다. */
263
- if (email !== this.email.trim()) {
264
- return;
265
- }
266
- this.lookup = response.data?.checkUserExistence ? 'found' : 'absent';
267
- }
268
- async sendInvitation(email) {
269
- this.working = true;
252
+ let exists;
270
253
  try {
271
- const response = await client.mutate({
272
- mutation: gql `
273
- mutation ($email: String!, $roleIds: [String!]) {
274
- sendInvitation(email: $email, roleIds: $roleIds) {
275
- id
276
- }
254
+ const response = await client.query({
255
+ query: gql `
256
+ query ($email: EmailAddress!) {
257
+ checkUserExistence(email: $email)
277
258
  }
278
259
  `,
279
- variables: { email, roleIds: [...this.selected] },
260
+ variables: { email },
280
261
  context: gqlContext()
281
262
  });
282
- if (response.errors?.length) {
283
- this.message = response.errors[0]?.message;
284
- return;
285
- }
286
- this.dispatchEvent(new CustomEvent('invited', { detail: { email }, bubbles: true, composed: true }));
287
- this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
263
+ exists = response.errors?.length ? undefined : response.data?.checkUserExistence;
288
264
  }
289
- finally {
290
- this.working = false;
265
+ catch (e) {
266
+ exists = undefined;
291
267
  }
292
- }
293
- async checkDefaultPassword() {
294
- const response = await client.query({
295
- query: gql `
296
- query {
297
- checkDefaultPassword
268
+ /* 묻는 사이에 사람이 더 쳤으면 그 답은 이미 옛것이다. */
269
+ if (email !== this.email.trim()) {
270
+ return;
298
271
  }
299
- `,
300
- context: gqlContext()
301
- });
302
- this.defaultPasswordConfigured = Boolean(response.data?.checkDefaultPassword);
272
+ /* 못 물었으면 「없다」가 아니다 — 없다고 그리면 있는 사람에게 계정을 만들러 간다. */
273
+ if (typeof exists !== 'boolean') {
274
+ this.lookup = 'failed';
275
+ this.message = i18next.t('text.could not look up the account');
276
+ return;
277
+ }
278
+ this.lookup = exists ? 'found' : 'absent';
303
279
  }
304
280
  async fetchRoles() {
305
281
  const response = await client.query({
@@ -328,14 +304,7 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
328
304
  return;
329
305
  }
330
306
  const email = this.email.trim();
331
- if (this.inviting) {
332
- /*
333
- * 초대는 여기서 끝난다 — 역할은 초대장이 싣고 가고, 수락할 때 함께 붙는다. 아래의
334
- * 「넣고 나서 역할」 두 호출과 달리 한 번이다.
335
- */
336
- await this.sendInvitation(email);
337
- return;
338
- }
307
+ const creating = this.creating;
339
308
  const username = this.creating ? this.usernameInput?.value.trim() || email : email;
340
309
  if (this.creating && !this.nameInput?.value.trim()) {
341
310
  this.message = i18next.t('text.x is required', { x: i18next.t('field.name') });
@@ -343,7 +312,7 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
343
312
  }
344
313
  this.working = true;
345
314
  try {
346
- const entered = this.creating ? await this.createAccount(email, username) : await this.addToDomain(username);
315
+ const entered = creating ? await this.createAccount(email, username) : await this.addToDomain(username);
347
316
  if (!entered) {
348
317
  return;
349
318
  }
@@ -371,7 +340,12 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
371
340
  context: gqlContext()
372
341
  });
373
342
  }
374
- this.dispatchEvent(new CustomEvent('user-invited', { detail: { username }, bubbles: true, composed: true }));
343
+ /* 새 계정은 「설정 대기」다 — 받는 쪽(목록)이 이어서 설정 링크를 건네게 한다(`detail.created`). */
344
+ this.dispatchEvent(new CustomEvent('user-invited', {
345
+ detail: { username, created: creating ? entered : null },
346
+ bubbles: true,
347
+ composed: true
348
+ }));
375
349
  }
376
350
  finally {
377
351
  this.working = false;
@@ -395,10 +369,12 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
395
369
  return true;
396
370
  }
397
371
  /**
398
- * 계정이 없는 사람 — 기본 비밀번호로 만든다.
372
+ * 계정이 없는 사람 — 「비밀번호 설정 대기」로 만든다. 비밀번호는 본인이 설정 링크로 정한다(ADR-0161).
399
373
  *
400
374
  * 역할은 여기서 안 넘긴다. `createUser` 는 **이 도메인 소유의 역할만** 붙이므로 상속·파트너
401
375
  * 역할이 조용히 빠진다. 부르는 쪽에서 `updateUserRoles` 로 한 번에 준다.
376
+ *
377
+ * @returns 만든 사람(설정 링크 팝업이 읽는 칸들), 못 만들었으면 null
402
378
  */
403
379
  async createAccount(email, username) {
404
380
  const response = await client.mutate({
@@ -406,6 +382,10 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
406
382
  mutation ($user: NewUser!) {
407
383
  createUser(user: $user) {
408
384
  id
385
+ username
386
+ email
387
+ name
388
+ status
409
389
  }
410
390
  }
411
391
  `,
@@ -421,9 +401,9 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
421
401
  });
422
402
  if (response.errors) {
423
403
  this.message = response.errors[0]?.message || '';
424
- return false;
404
+ return null;
425
405
  }
426
- return true;
406
+ return response.data?.createUser || null;
427
407
  }
428
408
  };
429
409
  __decorate([
@@ -450,10 +430,6 @@ __decorate([
450
430
  state(),
451
431
  __metadata("design:type", String)
452
432
  ], UserInvite.prototype, "message", void 0);
453
- __decorate([
454
- state(),
455
- __metadata("design:type", Boolean)
456
- ], UserInvite.prototype, "defaultPasswordConfigured", void 0);
457
433
  __decorate([
458
434
  query('[name=name]'),
459
435
  __metadata("design:type", HTMLInputElement)
@@ -1 +1 @@
1
- {"version":3,"file":"user-invite.js","sourceRoot":"","sources":["../../client/components/user-invite.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/elevated-button.js'\nimport '@material/web/textfield/filled-text-field.js'\nimport '@material/web/checkbox/checkbox.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, query, state } from 'lit/decorators.js'\n\nimport type { CSSResultGroup } from 'lit'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { InheritedValueType } from '@operato/shell'\nimport { ButtonContainerStyles, FormFieldStyles } from '@operato/styles'\n\n/**\n * 사람을 이 도메인으로 부르는 자리.\n *\n * ── 하나의 단추가 두 일을 덮고 있었다 ───────────────────────────────────────\n * 옛 화면의 「초대」는 `inviteUser` 를 부르는데, 그것이 하는 일은 **이미 계정이 있는 사람을 이\n * 도메인에 즉시 넣는 것**이다(`user-mutation.ts` 의 `inviteUser` — `invitee.domains` 에 도메인을\n * 더해 저장하고 끝난다). 메일도 가지 않고 상대의 동의도 없다.\n *\n * 그리고 계정이 없는 사람은 **거절했다** — 「사용자를 찾을 수 없습니다」. 관리자는 초대장을\n * 보냈다고 믿고 기다린다. 계정을 만드는 길은 화면의 **다른 자리**(`create-user`)에 있었고, 그\n * 자리는 기본 비밀번호가 설정된 도메인에서만 나타났다.\n *\n * ── 그래서 이메일 하나로 시작해 갈라 보여 준다 ──────────────────────────────\n * `checkUserExistence` 가 시스템 전체에서 그 이메일을 찾는다. 관리자는 「초대인가 생성인가」를\n * 먼저 고르지 않는다 — 이메일을 넣으면 화면이 무엇이 일어날지 말하고, 단추 글자도 그에 맞춘다.\n *\n * 계정이 있다 이 도메인에 바로 들어온다 (`inviteUser`)\n * 없고 기본 비밀번호 있다 계정을 만들어 넣는다 (`createUser`) — 비밀번호를 관리자가 전한다\n * 없고 기본 비밀번호 없다 **길이 없다.** 단추를 내주지 않고 무엇이 막고 있는지 말한다\n *\n * ⚠ **셋 중 어느 것도 「초대장」이 아니다.** 상대가 수락하는 흐름은 서버에 아직 없다\n * (`sendInvitation` 도 가입한 사용자만 받고, `Invitation` 에 상태도 만료도 없다). 그래서 이\n * 화면은 초대라는 말을 쓰지 않고 **실제로 일어나는 일**을 쓴다.\n *\n * ── 역할을 함께 고른다 ──────────────────────────────────────────────────────\n * 역할 없이 들어온 사람은 로그인해도 화면이 하나도 없다. 부르는 자리에서 함께 정하면 그 사람이\n * 처음 들어왔을 때 빈 화면을 보지 않는다.\n *\n * 넣기와 역할 부여는 **호출이 둘**이다(`inviteUser` 는 역할을 받지 않고, `createUser` 는 받지만\n * **이 도메인 소유의 역할만** 붙인다 — 상속 역할과 파트너 역할을 조용히 버린다). 그래서 두\n * 경로 모두 넣은 뒤 `updateUserRoles` 로 한 번에 준다. 앞이 되고 뒤가 실패하면 역할 없이 들어온\n * 상태가 되는데, 목록이 그것을 빨갛게 말하므로 관리자가 이어서 고칠 수 있다.\n */\n@customElement('user-invite')\nexport class UserInvite extends localize(i18next)(LitElement) {\n static styles: CSSResultGroup = [\n FormFieldStyles,\n ButtonContainerStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n min-width: min(420px, 86vw);\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n\n label {\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n }\n\n input {\n width: 100%;\n }\n\n [section] {\n margin-top: var(--spacing-medium, 8px);\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n }\n\n /* 색은 짝으로. 막고 있는 것만 눈에 걸리게 한다. */\n [note] {\n padding: 8px 10px;\n border-radius: var(--md-sys-shape-corner-small, 6px);\n font-size: 0.82rem;\n line-height: 1.5;\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n [note][attention] {\n background-color: var(--md-sys-color-error-container);\n color: var(--md-sys-color-on-error-container);\n }\n\n [invalid] {\n color: var(--md-sys-color-error);\n font-size: 0.8rem;\n min-height: 1.1rem;\n }\n\n ul[roles] {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n ul[roles] li {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 2px;\n }\n `\n ]\n\n @state() private email: string = ''\n @state() private lookup: 'idle' | 'checking' | 'found' | 'absent' = 'idle'\n @state() private roles: any[] = []\n @state() private selected: Set<string> = new Set()\n @state() private working: boolean = false\n @state() private message: string = ''\n\n /** 기본 비밀번호가 설정돼 있지 않으면 계정을 만드는 길이 없다 — 서버가 던진다. */\n @state() private defaultPasswordConfigured: boolean = false\n\n @query('[name=name]') private nameInput!: HTMLInputElement\n @query('[name=username]') private usernameInput!: HTMLInputElement\n\n private timer?: ReturnType<typeof setTimeout>\n\n /** 계정을 새로 만드는 경로인가. */\n private get creating(): boolean {\n return this.lookup === 'absent' && this.defaultPasswordConfigured\n }\n\n /**\n * 메일로 초대하는 경로인가.\n *\n * 계정이 없고 기본 비밀번호도 없을 때다. 관리자가 비밀번호를 대신 정해 줄 수 없으므로\n * **본인이 메일을 받아 스스로 정하는 것이 유일한 길**이다.\n */\n private get inviting(): boolean {\n return this.lookup === 'absent' && !this.defaultPasswordConfigured\n }\n\n /** 부를 수 있나 — 단추와 처리기가 같은 답을 본다. */\n private get callable(): boolean {\n return !this.working && (this.lookup === 'found' || this.creating || this.inviting)\n }\n\n render() {\n return html`\n <md-filled-text-field\n id=\"email\"\n name=\"email\"\n type=\"email\"\n label=${i18next.t('field.email')}\n .value=${this.email}\n placeholder=\"name@company.com\"\n @input=${(e: Event) => this.onEmail((e.target as HTMLInputElement).value)}\n ></md-filled-text-field>\n <div invalid>${this.message}</div>\n\n ${this.renderLookup()} ${this.creating ? this.renderNewAccount() : nothing}\n ${this.lookup === 'found' || this.creating || this.inviting ? this.renderRoles() : nothing}\n\n <div class=\"button-container\">\n <md-elevated-button ?disabled=${!this.callable} @click=${() => this.onCall()}>\n ${this.inviting\n ? i18next.t('label.send invitation')\n : this.creating\n ? i18next.t('label.create account and add')\n : i18next.t('label.add to domain')}\n </md-elevated-button>\n <md-elevated-button @click=${() => this.dispatchEvent(new CustomEvent('close'))}>\n ${i18next.t('button.cancel')}\n </md-elevated-button>\n </div>\n `\n }\n\n private renderLookup() {\n switch (this.lookup) {\n case 'checking':\n return html`<div note>${i18next.t('text.loading')}</div>`\n case 'found':\n return html`<div note>${i18next.t('text.account exists so joins immediately')}</div>`\n case 'absent':\n return this.defaultPasswordConfigured\n ? html`<div note>${i18next.t('text.account will be created with the default password')}</div>`\n : /*\n * 전에는 여기가 막다른 자리였다 — 관리자가 비밀번호를 대신 정해 줄 수 없으니 계정을\n * 만들 길이 없었다. 이제 초대장이 그 길이다. 본인이 메일을 받아 스스로 비밀번호를\n * 정하고 들어온다.\n */\n html`<div note>${i18next.t('text.no account so an invitation will be mailed')}</div>`\n default:\n return nothing\n }\n }\n\n private renderNewAccount() {\n return html`\n <md-filled-text-field\n id=\"new-name\"\n name=\"name\"\n type=\"text\"\n label=${i18next.t('field.name')}\n @input=${() => this.requestUpdate()}\n ></md-filled-text-field>\n\n <!-- 서버가 받는 글자는 A-Z a-z 0-9 . _ @ - 다. 비우면 이메일을 그대로 쓴다. -->\n <md-filled-text-field\n id=\"new-username\"\n name=\"username\"\n type=\"text\"\n label=${i18next.t('field.username')}\n placeholder=${this.email}\n ></md-filled-text-field>\n `\n }\n\n private renderRoles() {\n return html`\n <div section>${i18next.t('field.roles')}</div>\n ${\n this.roles.length\n ? html`<ul roles>\n ${this.roles.map(\n role => html`\n <li>\n <md-checkbox\n id=\"invite-role-${role.id}\"\n .checked=${this.selected.has(role.id)}\n @change=${(e: Event) => this.onToggle(role.id, (e.target as HTMLInputElement).checked)}\n ></md-checkbox>\n <label for=\"invite-role-${role.id}\">${role.name}</label>\n </li>\n `\n )}\n </ul>`\n : html`<div note attention>${i18next.t('text.no role defined in this domain')}</div>`\n }\n ${\n this.roles.length && this.selected.size === 0\n ? html`<div note attention>${i18next.t('text.without a role they see nothing')}</div>`\n : nothing\n }\n `\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.fetchRoles()\n this.checkDefaultPassword()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n clearTimeout(this.timer)\n }\n\n private onEmail(value: string) {\n this.email = value\n this.message = ''\n this.lookup = 'idle'\n\n clearTimeout(this.timer)\n\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim())) {\n return\n }\n\n /* 한 글자마다 묻지 않는다. 사람이 멈추면 묻는다. */\n this.timer = setTimeout(() => this.check(), 400)\n }\n\n private async check() {\n const email = this.email.trim()\n this.lookup = 'checking'\n\n const response = await client.query({\n query: gql`\n query ($email: EmailAddress!) {\n checkUserExistence(email: $email)\n }\n `,\n variables: { email },\n context: gqlContext()\n })\n\n /* 묻는 사이에 사람이 더 쳤으면 그 답은 이미 옛것이다. */\n if (email !== this.email.trim()) {\n return\n }\n\n this.lookup = response.data?.checkUserExistence ? 'found' : 'absent'\n }\n\n private async sendInvitation(email: string) {\n this.working = true\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($email: String!, $roleIds: [String!]) {\n sendInvitation(email: $email, roleIds: $roleIds) {\n id\n }\n }\n `,\n variables: { email, roleIds: [...this.selected] },\n context: gqlContext()\n })\n\n if (response.errors?.length) {\n this.message = response.errors[0]?.message\n return\n }\n\n this.dispatchEvent(new CustomEvent('invited', { detail: { email }, bubbles: true, composed: true }))\n this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))\n } finally {\n this.working = false\n }\n }\n\n private async checkDefaultPassword() {\n const response = await client.query({\n query: gql`\n query {\n checkDefaultPassword\n }\n `,\n context: gqlContext()\n })\n\n this.defaultPasswordConfigured = Boolean(response.data?.checkDefaultPassword)\n }\n\n private async fetchRoles() {\n const response = await client.query({\n query: gql`\n query ($inherited: InheritedValueType) {\n roles(inherited: $inherited) {\n items {\n id\n name\n }\n }\n }\n `,\n variables: { inherited: InheritedValueType.Include },\n context: gqlContext()\n })\n\n this.roles = response.data?.roles?.items || []\n }\n\n private onToggle(id: string, checked: boolean) {\n const next = new Set(this.selected)\n checked ? next.add(id) : next.delete(id)\n this.selected = next\n }\n\n private async onCall() {\n if (!this.callable) {\n return\n }\n\n const email = this.email.trim()\n\n if (this.inviting) {\n /*\n * 초대는 여기서 끝난다 — 역할은 초대장이 싣고 가고, 수락할 때 함께 붙는다. 아래의\n * 「넣고 나서 역할」 두 호출과 달리 한 번이다.\n */\n await this.sendInvitation(email)\n return\n }\n\n const username = this.creating ? this.usernameInput?.value.trim() || email : email\n\n if (this.creating && !this.nameInput?.value.trim()) {\n this.message = i18next.t('text.x is required', { x: i18next.t('field.name') })\n return\n }\n\n this.working = true\n\n try {\n const entered = this.creating ? await this.createAccount(email, username) : await this.addToDomain(username)\n\n if (!entered) {\n return\n }\n\n if (this.selected.size) {\n /*\n * 넣기와 역할 부여가 두 호출이다. 여기가 실패하면 그 사람은 역할 없이 들어와 있고,\n * 목록이 그것을 빨갛게 말한다 — 사라지지 않는다.\n *\n * `createUser` 도 역할을 받지만 **이 도메인 소유의 역할만** 붙인다. 상속 역할과 파트너\n * 역할을 조용히 버리므로, 고른 것을 그대로 주려면 이 호출을 쓴다.\n */\n await client.mutate({\n mutation: gql`\n mutation ($username: String!, $availableRoles: [ObjectRef!]!, $selectedRoles: [ObjectRef!]!) {\n updateUserRoles(username: $username, availableRoles: $availableRoles, selectedRoles: $selectedRoles) {\n id\n }\n }\n `,\n variables: {\n username,\n availableRoles: this.roles.map(role => ({ id: role.id })),\n selectedRoles: this.roles.filter(role => this.selected.has(role.id)).map(role => ({ id: role.id }))\n },\n context: gqlContext()\n })\n }\n\n this.dispatchEvent(new CustomEvent('user-invited', { detail: { username }, bubbles: true, composed: true }))\n } finally {\n this.working = false\n }\n }\n\n /** 이미 계정이 있는 사람 — 이 도메인에 바로 들어온다. */\n private async addToDomain(username: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($username: String!) {\n inviteUser(username: $username)\n }\n `,\n variables: { username },\n context: gqlContext()\n })\n\n if (response.errors) {\n this.message = response.errors[0]?.message || ''\n return false\n }\n\n return true\n }\n\n /**\n * 계정이 없는 사람 — 기본 비밀번호로 만든다.\n *\n * 역할은 여기서 안 넘긴다. `createUser` 는 **이 도메인 소유의 역할만** 붙이므로 상속·파트너\n * 역할이 조용히 빠진다. 부르는 쪽에서 `updateUserRoles` 로 한 번에 준다.\n */\n private async createAccount(email: string, username: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($user: NewUser!) {\n createUser(user: $user) {\n id\n }\n }\n `,\n variables: {\n user: {\n username,\n name: this.nameInput.value.trim(),\n email,\n userType: 'user'\n }\n },\n context: gqlContext()\n })\n\n if (response.errors) {\n this.message = response.errors[0]?.message || ''\n return false\n }\n\n return true\n }\n}\n"]}
1
+ {"version":3,"file":"user-invite.js","sourceRoot":"","sources":["../../client/components/user-invite.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@operato/popup/ox-popup-actions.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, query, state } from 'lit/decorators.js'\n\nimport type { CSSResultGroup } from 'lit'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { InheritedValueType } from '@operato/shell'\nimport { CommonPopupStyles, FormFieldStyles, PopupBodyStyles, PopupFieldStyles } from '@operato/styles'\n\n/**\n * 사람을 이 도메인으로 부르는 자리.\n *\n * ── 하나의 단추가 두 일을 덮고 있었다 ───────────────────────────────────────\n * 옛 화면의 「초대」는 `inviteUser` 를 부르는데, 그것이 하는 일은 **이미 계정이 있는 사람을 이\n * 도메인에 즉시 넣는 것**이다(`user-mutation.ts` 의 `inviteUser` — `invitee.domains` 에 도메인을\n * 더해 저장하고 끝난다). 메일도 가지 않고 상대의 동의도 없다.\n *\n * 그리고 계정이 없는 사람은 **거절했다** — 「사용자를 찾을 수 없습니다」. 관리자는 초대장을\n * 보냈다고 믿고 기다린다. 계정을 만드는 길은 화면의 **다른 자리**(`create-user`)에 있었고, 그\n * 자리는 기본 비밀번호가 설정된 도메인에서만 나타났다.\n *\n * ── 그래서 이메일 하나로 시작해 갈라 보여 준다 ──────────────────────────────\n * `checkUserExistence` 가 시스템 전체에서 그 이메일을 찾는다. 관리자는 「넣기인가 만들기인가」를\n * 먼저 고르지 않는다 — 이메일을 넣으면 화면이 무엇이 일어날지 말하고, 단추 글자도 그에 맞춘다.\n *\n * 계정이 있다 이 도메인에 바로 들어온다 (`inviteUser`)\n * 계정이 없다 「비밀번호 설정 대기」 계정을 만들고(`createUser`), 이어서 비밀번호 설정 링크를\n * 건넨다(`password-setup-link-popup`). 관리자는 비밀번호를 정하지 않는다(ADR-0161).\n *\n * ── 역할을 함께 고른다 ──────────────────────────────────────────────────────\n * 역할 없이 들어온 사람은 로그인해도 화면이 하나도 없다. 부르는 자리에서 함께 정하면 그 사람이\n * 처음 들어왔을 때 빈 화면을 보지 않는다.\n *\n * 넣기와 역할 부여는 **호출이 둘**이다(`inviteUser` 는 역할을 받지 않고, `createUser` 는 받지만\n * **이 도메인 소유의 역할만** 붙인다 — 상속 역할과 파트너 역할을 조용히 버린다). 그래서 두\n * 경로 모두 넣은 뒤 `updateUserRoles` 로 한 번에 준다. 앞이 되고 뒤가 실패하면 역할 없이 들어온\n * 상태가 되는데, 목록이 그것을 빨갛게 말하므로 관리자가 이어서 고칠 수 있다.\n */\n@customElement('user-invite')\nexport class UserInvite extends localize(i18next)(LitElement) {\n /* 팝업의 집 모양(page-standard §7·§8): 본문만 스크롤, 칸은 라벨 위의 날것 입력, 단추 줄은 `ox-popup-actions`. */\n static styles: CSSResultGroup = [\n CommonPopupStyles,\n PopupBodyStyles,\n FormFieldStyles,\n PopupFieldStyles,\n css`\n /* 오류 문장은 칸 바로 아래(§7). */\n [invalid] {\n color: var(--ui-crit, var(--md-sys-color-error));\n font-size: var(--ui-t-small, 11.5px);\n }\n\n [invalid]:empty {\n display: none;\n }\n\n [guide] p {\n margin: 0 0 var(--spacing-small, 4px);\n line-height: var(--popup-line-height);\n }\n\n [guide] dl {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 2px var(--spacing-large, 12px);\n margin: 0;\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n line-height: var(--popup-line-height);\n }\n\n [guide] dl > div {\n display: contents;\n }\n\n [guide] dt {\n font-weight: 600;\n }\n\n [guide] dd {\n margin: 0;\n }\n\n /* 넣은 이메일이 해당하는 갈래 — 띠로 짚는다. */\n [guide] dl > div > * {\n padding: 1px var(--spacing-small, 4px);\n }\n\n [guide] dl > div[current] > * {\n background: var(--ui-line-soft, var(--md-sys-color-surface-container));\n color: var(--ui-ink);\n }\n\n [group-label] {\n margin-top: var(--spacing-medium, 8px);\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n ul[roles] {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n ul[roles] li {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: 3px 0;\n }\n `\n ]\n\n @state() private email: string = ''\n @state() private lookup: 'idle' | 'checking' | 'found' | 'absent' | 'failed' = 'idle'\n @state() private roles: any[] = []\n @state() private selected: Set<string> = new Set()\n @state() private working: boolean = false\n @state() private message: string = ''\n\n @query('[name=name]') private nameInput!: HTMLInputElement\n @query('[name=username]') private usernameInput!: HTMLInputElement\n\n private timer?: ReturnType<typeof setTimeout>\n\n /** 계정을 새로 만드는 경로인가. */\n private get creating(): boolean {\n return this.lookup === 'absent'\n }\n\n /** 부를 수 있나 — 단추와 처리기가 같은 답을 본다. */\n private get callable(): boolean {\n return !this.working && (this.lookup === 'found' || this.creating)\n }\n\n render() {\n return html`\n <div class=\"body\">\n ${this.renderGuide()}\n\n <label class=\"field\">\n <span>${i18next.t('field.email')}</span>\n <input\n name=\"email\"\n type=\"email\"\n .value=${this.email}\n placeholder=\"name@company.com\"\n @input=${(e: Event) => this.onEmail((e.target as HTMLInputElement).value)}\n />\n <span invalid>${this.message}</span>\n </label>\n\n ${this.lookup === 'checking' ? html`<div class=\"note\">${i18next.t('text.loading')}</div>` : nothing}\n ${this.creating ? this.renderNewAccount() : nothing}\n ${this.lookup === 'found' || this.creating ? this.renderRoles() : nothing}\n </div>\n\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => this.dispatchEvent(new CustomEvent('close'))}>\n ${i18next.t('button.cancel')}\n </md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${!this.callable} @click=${() => this.onCall()}>\n ${this.creating ? i18next.t('label.create account and add') : i18next.t('label.add to domain')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n /**\n * 사용자 추가가 무엇인지 — 이메일을 넣기 전에 말한다.\n *\n * 칸 하나만 있으면 이 팝업이 무엇을 하는지 알 수 없다. 계정은 하나로 여러 도메인에서 쓰고, 추가는\n * 그 계정을 이 도메인에 넣는 일이다. 이메일에 따라 일어나는 일이 갈리므로 갈래를 먼저 보이고,\n * 이메일을 찾으면 해당하는 줄을 짚는다.\n */\n private renderGuide() {\n return html`\n <div guide>\n <p>${i18next.t('text.add user puts an account into this domain')}</p>\n <dl>\n <div ?current=${this.lookup === 'found'}>\n <dt>${i18next.t('label.account exists')}</dt>\n <dd>${i18next.t('text.they join this domain at once')}</dd>\n </div>\n <div ?current=${this.lookup === 'absent'}>\n <dt>${i18next.t('label.no account')}</dt>\n <dd>${i18next.t('text.an account is made and a setup link handed over')}</dd>\n </div>\n <div>\n <dt>${i18next.t('field.roles')}</dt>\n <dd>${i18next.t('text.roles decide the screens they open')}</dd>\n </div>\n </dl>\n </div>\n `\n }\n\n private renderNewAccount() {\n return html`\n <label class=\"field\">\n <span>${i18next.t('field.name')}</span>\n <input name=\"name\" type=\"text\" @input=${() => this.requestUpdate()} />\n </label>\n\n <!-- 서버가 받는 글자는 A-Z a-z 0-9 . _ @ - 다. 비우면 이메일을 그대로 쓴다. -->\n <label class=\"field\">\n <span>${i18next.t('field.username')}</span>\n <input name=\"username\" type=\"text\" placeholder=${this.email} />\n </label>\n `\n }\n\n private renderRoles() {\n return html`\n <div group-label>${i18next.t('field.roles')}</div>\n ${\n this.roles.length\n ? html`<ul roles>\n ${this.roles.map(\n role => html`\n <li>\n <input\n type=\"checkbox\"\n id=\"invite-role-${role.id}\"\n .checked=${this.selected.has(role.id)}\n @change=${(e: Event) => this.onToggle(role.id, (e.target as HTMLInputElement).checked)}\n />\n <label for=\"invite-role-${role.id}\">${role.name}</label>\n </li>\n `\n )}\n </ul>`\n : html`<div class=\"failure\">${i18next.t('text.no role defined in this domain')}</div>`\n }\n ${\n this.roles.length && this.selected.size === 0\n ? html`<div class=\"failure\">${i18next.t('text.without a role they see nothing')}</div>`\n : nothing\n }\n `\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.fetchRoles()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n clearTimeout(this.timer)\n }\n\n private onEmail(value: string) {\n this.email = value\n this.message = ''\n this.lookup = 'idle'\n\n clearTimeout(this.timer)\n\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim())) {\n return\n }\n\n /* 한 글자마다 묻지 않는다. 사람이 멈추면 묻는다. */\n this.timer = setTimeout(() => this.check(), 400)\n }\n\n private async check() {\n const email = this.email.trim()\n this.lookup = 'checking'\n\n let exists: boolean | undefined\n\n try {\n const response = await client.query({\n query: gql`\n query ($email: EmailAddress!) {\n checkUserExistence(email: $email)\n }\n `,\n variables: { email },\n context: gqlContext()\n })\n\n exists = response.errors?.length ? undefined : response.data?.checkUserExistence\n } catch (e) {\n exists = undefined\n }\n\n /* 묻는 사이에 사람이 더 쳤으면 그 답은 이미 옛것이다. */\n if (email !== this.email.trim()) {\n return\n }\n\n /* 못 물었으면 「없다」가 아니다 — 없다고 그리면 있는 사람에게 계정을 만들러 간다. */\n if (typeof exists !== 'boolean') {\n this.lookup = 'failed'\n this.message = i18next.t('text.could not look up the account')\n return\n }\n\n this.lookup = exists ? 'found' : 'absent'\n }\n\n private async fetchRoles() {\n const response = await client.query({\n query: gql`\n query ($inherited: InheritedValueType) {\n roles(inherited: $inherited) {\n items {\n id\n name\n }\n }\n }\n `,\n variables: { inherited: InheritedValueType.Include },\n context: gqlContext()\n })\n\n this.roles = response.data?.roles?.items || []\n }\n\n private onToggle(id: string, checked: boolean) {\n const next = new Set(this.selected)\n checked ? next.add(id) : next.delete(id)\n this.selected = next\n }\n\n private async onCall() {\n if (!this.callable) {\n return\n }\n\n const email = this.email.trim()\n const creating = this.creating\n const username = this.creating ? this.usernameInput?.value.trim() || email : email\n\n if (this.creating && !this.nameInput?.value.trim()) {\n this.message = i18next.t('text.x is required', { x: i18next.t('field.name') })\n return\n }\n\n this.working = true\n\n try {\n const entered = creating ? await this.createAccount(email, username) : await this.addToDomain(username)\n\n if (!entered) {\n return\n }\n\n if (this.selected.size) {\n /*\n * 넣기와 역할 부여가 두 호출이다. 여기가 실패하면 그 사람은 역할 없이 들어와 있고,\n * 목록이 그것을 빨갛게 말한다 — 사라지지 않는다.\n *\n * `createUser` 도 역할을 받지만 **이 도메인 소유의 역할만** 붙인다. 상속 역할과 파트너\n * 역할을 조용히 버리므로, 고른 것을 그대로 주려면 이 호출을 쓴다.\n */\n await client.mutate({\n mutation: gql`\n mutation ($username: String!, $availableRoles: [ObjectRef!]!, $selectedRoles: [ObjectRef!]!) {\n updateUserRoles(username: $username, availableRoles: $availableRoles, selectedRoles: $selectedRoles) {\n id\n }\n }\n `,\n variables: {\n username,\n availableRoles: this.roles.map(role => ({ id: role.id })),\n selectedRoles: this.roles.filter(role => this.selected.has(role.id)).map(role => ({ id: role.id }))\n },\n context: gqlContext()\n })\n }\n\n /* 새 계정은 「설정 대기」다 — 받는 쪽(목록)이 이어서 설정 링크를 건네게 한다(`detail.created`). */\n this.dispatchEvent(\n new CustomEvent('user-invited', {\n detail: { username, created: creating ? entered : null },\n bubbles: true,\n composed: true\n })\n )\n } finally {\n this.working = false\n }\n }\n\n /** 이미 계정이 있는 사람 — 이 도메인에 바로 들어온다. */\n private async addToDomain(username: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($username: String!) {\n inviteUser(username: $username)\n }\n `,\n variables: { username },\n context: gqlContext()\n })\n\n if (response.errors) {\n this.message = response.errors[0]?.message || ''\n return false\n }\n\n return true\n }\n\n /**\n * 계정이 없는 사람 — 「비밀번호 설정 대기」로 만든다. 비밀번호는 본인이 설정 링크로 정한다(ADR-0161).\n *\n * 역할은 여기서 안 넘긴다. `createUser` 는 **이 도메인 소유의 역할만** 붙이므로 상속·파트너\n * 역할이 조용히 빠진다. 부르는 쪽에서 `updateUserRoles` 로 한 번에 준다.\n *\n * @returns 만든 사람(설정 링크 팝업이 읽는 칸들), 못 만들었으면 null\n */\n private async createAccount(email: string, username: string): Promise<any> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($user: NewUser!) {\n createUser(user: $user) {\n id\n username\n email\n name\n status\n }\n }\n `,\n variables: {\n user: {\n username,\n name: this.nameInput.value.trim(),\n email,\n userType: 'user'\n }\n },\n context: gqlContext()\n })\n\n if (response.errors) {\n this.message = response.errors[0]?.message || ''\n return null\n }\n\n return response.data?.createUser || null\n }\n}\n"]}