@things-factory/auth-ui 8.0.0-alpha.8 → 8.0.0-beta.1

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 (69) hide show
  1. package/client/components/abstract-auth-page.ts +10 -10
  2. package/client/components/abstract-password-reset.ts +9 -14
  3. package/client/components/abstract-sign.ts +13 -13
  4. package/client/components/change-password.ts +1 -0
  5. package/client/components/contact-us.ts +11 -8
  6. package/client/components/create-role.ts +1 -0
  7. package/client/components/create-user.ts +27 -5
  8. package/client/components/invite-user.ts +19 -10
  9. package/client/components/ownership-transfer-popup.ts +3 -3
  10. package/client/components/profile-component.ts +68 -4
  11. package/client/components/role-privilege-editor.ts +34 -6
  12. package/client/components/user-role-editor.ts +20 -20
  13. package/client/entries/auth/checkin.ts +1 -1
  14. package/client/entries/auth/forgot-password.ts +11 -2
  15. package/client/entries/auth/signup.ts +13 -7
  16. package/client/index.ts +1 -1
  17. package/client/pages/attribute/attribute-set-item-list.ts +1 -1
  18. package/client/pages/role/role-management.ts +2 -0
  19. package/client/pages/user/user-management.ts +10 -11
  20. package/dist-client/components/abstract-auth-page.js +10 -10
  21. package/dist-client/components/abstract-auth-page.js.map +1 -1
  22. package/dist-client/components/abstract-password-reset.d.ts +1 -2
  23. package/dist-client/components/abstract-password-reset.js +7 -14
  24. package/dist-client/components/abstract-password-reset.js.map +1 -1
  25. package/dist-client/components/abstract-sign.js +12 -11
  26. package/dist-client/components/abstract-sign.js.map +1 -1
  27. package/dist-client/components/change-password.js +1 -0
  28. package/dist-client/components/change-password.js.map +1 -1
  29. package/dist-client/components/contact-us.d.ts +1 -1
  30. package/dist-client/components/contact-us.js +10 -7
  31. package/dist-client/components/contact-us.js.map +1 -1
  32. package/dist-client/components/create-role.js +1 -0
  33. package/dist-client/components/create-role.js.map +1 -1
  34. package/dist-client/components/create-user.js +28 -5
  35. package/dist-client/components/create-user.js.map +1 -1
  36. package/dist-client/components/invite-user.js +20 -11
  37. package/dist-client/components/invite-user.js.map +1 -1
  38. package/dist-client/components/ownership-transfer-popup.js +3 -3
  39. package/dist-client/components/ownership-transfer-popup.js.map +1 -1
  40. package/dist-client/components/profile-component.d.ts +5 -1
  41. package/dist-client/components/profile-component.js +68 -4
  42. package/dist-client/components/profile-component.js.map +1 -1
  43. package/dist-client/components/role-privilege-editor.js +41 -10
  44. package/dist-client/components/role-privilege-editor.js.map +1 -1
  45. package/dist-client/components/user-role-editor.js +20 -20
  46. package/dist-client/components/user-role-editor.js.map +1 -1
  47. package/dist-client/entries/auth/checkin.js +1 -1
  48. package/dist-client/entries/auth/checkin.js.map +1 -1
  49. package/dist-client/entries/auth/forgot-password.js +11 -2
  50. package/dist-client/entries/auth/forgot-password.js.map +1 -1
  51. package/dist-client/entries/auth/signup.js +13 -7
  52. package/dist-client/entries/auth/signup.js.map +1 -1
  53. package/dist-client/index.js +1 -1
  54. package/dist-client/index.js.map +1 -1
  55. package/dist-client/pages/attribute/attribute-set-item-list.js +1 -1
  56. package/dist-client/pages/attribute/attribute-set-item-list.js.map +1 -1
  57. package/dist-client/pages/role/role-management.js +2 -0
  58. package/dist-client/pages/role/role-management.js.map +1 -1
  59. package/dist-client/pages/user/user-management.d.ts +29 -0
  60. package/dist-client/pages/user/user-management.js +9 -9
  61. package/dist-client/pages/user/user-management.js.map +1 -1
  62. package/dist-client/tsconfig.tsbuildinfo +1 -1
  63. package/dist-server/tsconfig.tsbuildinfo +1 -1
  64. package/package.json +12 -12
  65. package/translations/en.json +8 -2
  66. package/translations/ja.json +7 -2
  67. package/translations/ko.json +7 -2
  68. package/translations/ms.json +8 -2
  69. package/translations/zh.json +8 -2
@@ -176,22 +176,22 @@ export abstract class AbstractAuthPage extends localize(i18next)(LitElement) {
176
176
  }
177
177
 
178
178
  get formfields() {
179
- const email = this.data?.email || ''
180
- // .validationMessage=${String(i18next.t('text.invalid-email'))}
179
+ const username = this.data?.username || ''
180
+ // .validationMessage=${String(i18next.t('text.invalid-username'))}
181
181
 
182
182
  return html`
183
183
  <input id="redirectTo" type="hidden" name="redirectTo" .value=${this.redirectTo || '/'} />
184
184
 
185
185
  <div class="field">
186
186
  <md-filled-text-field
187
- name="email"
188
- type="email"
189
- label=${String(i18next.t('field.email'))}
187
+ name="username"
188
+ type="text"
189
+ label=${String(i18next.t('field.user-id or email'))}
190
190
  required
191
- .value=${email}
191
+ .value=${username}
192
192
  autocomplete="off"
193
193
  autocapitalize="off"
194
- ><md-icon slot="leading-icon">mail</md-icon></md-filled-text-field
194
+ ><md-icon slot="leading-icon">id_card</md-icon></md-filled-text-field
195
195
  >
196
196
  </div>
197
197
  <div class="field">
@@ -201,11 +201,11 @@ export abstract class AbstractAuthPage extends localize(i18next)(LitElement) {
201
201
  label=${String(i18next.t('field.password'))}
202
202
  autocomplete="off"
203
203
  required
204
- ><md-icon slot="leading-icon">vpn_key</md-icon></md-filled-text-field
204
+ ><md-icon slot="leading-icon">password</md-icon></md-filled-text-field
205
205
  >
206
206
  </div>
207
207
 
208
- <md-elevated-button class="ui" type="submit" raised @click=${e => this._onSubmit(e)}>
208
+ <md-elevated-button class="ui" type="button" raised @click=${e => this._onSubmit(e)}>
209
209
  <ox-i18n msgid="field.${this.pageName}"> </ox-i18n>
210
210
  </md-elevated-button>
211
211
  `
@@ -235,7 +235,7 @@ export abstract class AbstractAuthPage extends localize(i18next)(LitElement) {
235
235
  sso => html`
236
236
  <a class="link" href=${sso.link}>
237
237
  <md-text-button>
238
- <md-icon slot="icon">badge</md-icon>
238
+ <md-icon slot="icon">id_card</md-icon>
239
239
  ${i18next.t('label.signin with', { title: sso.title })}
240
240
  </md-text-button>
241
241
  </a>
@@ -2,17 +2,16 @@ import '@material/web/icon/icon.js'
2
2
  import '@material/web/button/elevated-button.js'
3
3
  import '@material/web/textfield/filled-text-field.js'
4
4
 
5
- import '@operato/lottie-player'
6
- import '../components/profile-component'
7
5
  import '@operato/i18n/ox-i18n.js'
8
6
  import '@operato/i18n/ox-i18n-selector.js'
9
7
  import '@operato/layout/ox-snack-bar.js'
10
8
 
9
+ import '../components/profile-component'
10
+
11
11
  import { css, html, nothing } from 'lit'
12
12
  import { property, query } from 'lit/decorators.js'
13
13
 
14
14
  import { i18next } from '@operato/i18n'
15
- import { isSafari } from '@operato/utils'
16
15
 
17
16
  import { AUTH_STYLE_SIGN } from '../auth-style-sign'
18
17
  import { generatePasswordPatternHelp, generatePasswordPatternRegExp } from '../utils/password-rule'
@@ -91,7 +90,8 @@ export abstract class AbstractPasswordReset extends AbstractAuthPage {
91
90
  this.confirmPass.setAttribute('pattern', val.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '[$&]'))
92
91
  }}
93
92
  required
94
- ></md-filled-text-field>
93
+ ><md-icon slot="leading-icon">password</md-icon></md-filled-text-field
94
+ >
95
95
  </div>
96
96
 
97
97
  <div class="field">
@@ -102,10 +102,11 @@ export abstract class AbstractPasswordReset extends AbstractAuthPage {
102
102
  label=${String(i18next.t('field.confirm password'))}
103
103
  autocomplete="off"
104
104
  required
105
- ></md-filled-text-field>
105
+ ><md-icon slot="leading-icon">password</md-icon></md-filled-text-field
106
+ >
106
107
  </div>
107
108
 
108
- <md-elevated-button id="submit-button" type="submit" @click=${e => this._onSubmit(e)}>
109
+ <md-elevated-button id="submit-button" type="button" @click=${e => this._onSubmit(e)}>
109
110
  <ox-i18n msgid="${this.submitButtonLabel}"></ox-i18n>
110
111
  </md-elevated-button>
111
112
 
@@ -128,20 +129,14 @@ export abstract class AbstractPasswordReset extends AbstractAuthPage {
128
129
  </form>
129
130
  </div>
130
131
  </div>
131
- <ox-snack-bar id="snackbar" level="error" .message=${this.message}></ox-snack-bar>
132
132
 
133
- ${isSafari()
134
- ? html``
135
- : html`
136
- <div class="lottie-container">
137
- <lottie-player autoplay loop src="../../assets/images/background-animation.json"></lottie-player>
138
- </div>
139
- `}
133
+ <ox-snack-bar id="snackbar" level="error" .message=${this.message}></ox-snack-bar>
140
134
  `
141
135
  }
142
136
 
143
137
  updated(changed) {
144
138
  super.updated(changed)
139
+
145
140
  if (changed.has('data')) {
146
141
  this.token = this.data.token
147
142
  }
@@ -44,7 +44,7 @@ export abstract class AbstractSign extends AbstractAuthPage {
44
44
  }
45
45
 
46
46
  get formfields() {
47
- const email = this.data?.email || ''
47
+ const username = this.data?.username || ''
48
48
  const autocompletable = this.autocompletable
49
49
 
50
50
  return html`
@@ -52,22 +52,23 @@ export abstract class AbstractSign extends AbstractAuthPage {
52
52
 
53
53
  <div class="field">
54
54
  <md-filled-text-field
55
- name="email"
56
- type="email"
57
- label=${String(i18next.t('field.email'))}
55
+ name="username"
56
+ type="text"
57
+ label=${String(i18next.t('field.user-id or email'))}
58
58
  required
59
- .value=${email}
60
- autocomplete=${autocompletable ? "username" : "off"}
59
+ .value=${username}
60
+ autocomplete=${autocompletable ? 'username' : 'off'}
61
61
  autocapitalize="off"
62
+ pattern="^(?:[A-Za-z0-9]*|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,})$"
62
63
  @input=${(e: Event) => {
63
64
  const target = e.target as HTMLInputElement
64
65
  if (target.validity.typeMismatch) {
65
- target.setCustomValidity(i18next.t('text.invalid-email'))
66
+ target.setCustomValidity(i18next.t('text.invalid-username'))
66
67
  } else {
67
68
  target.setCustomValidity('')
68
69
  }
69
70
  }}
70
- ><md-icon slot="leading-icon">mail</md-icon></md-filled-text-field
71
+ ><md-icon slot="leading-icon">id_card</md-icon></md-filled-text-field
71
72
  >
72
73
  </div>
73
74
  <div class="field">
@@ -75,14 +76,14 @@ export abstract class AbstractSign extends AbstractAuthPage {
75
76
  name="password"
76
77
  type="password"
77
78
  label=${String(i18next.t('field.password'))}
78
- autocomplete=${autocompletable ? "current-password" : "off"}
79
+ autocomplete=${autocompletable ? 'current-password' : 'off'}
79
80
  required
80
- ><md-icon slot="leading-icon">vpn_key</md-icon></md-filled-text-field
81
+ ><md-icon slot="leading-icon">password</md-icon></md-filled-text-field
81
82
  >
82
83
  </div>
83
84
 
84
85
  <div class="submit-buttons-container">
85
- <md-elevated-button class="submit-button" type="submit" raised @click=${e => this._onSubmit(e)}>
86
+ <md-elevated-button class="submit-button" type="button" raised @click=${e => this._onSubmit(e)}>
86
87
  <ox-i18n msgid="field.${this.pageName}"> </ox-i18n>
87
88
  </md-elevated-button>
88
89
  ${isAvailableWebauthn
@@ -106,7 +107,7 @@ export abstract class AbstractSign extends AbstractAuthPage {
106
107
 
107
108
  if (verification.verified) {
108
109
  const { redirectURL } = verification
109
-
110
+
110
111
  if (redirectURL) {
111
112
  window.location.href = redirectURL
112
113
  }
@@ -116,7 +117,6 @@ export abstract class AbstractSign extends AbstractAuthPage {
116
117
  message: verification.message
117
118
  })
118
119
  }
119
-
120
120
  } catch (error) {
121
121
  notify({
122
122
  level: 'error',
@@ -43,6 +43,7 @@ export class ChangePassword extends localize(i18next)(LitElement) {
43
43
 
44
44
  md-elevated-button {
45
45
  margin: var(--spacing-small) auto var(--spacing-medium) auto;
46
+ text-transform: capitalize;
46
47
  }
47
48
 
48
49
  button {
@@ -33,22 +33,23 @@ export class ContactUs extends localize(i18next)(LitElement) {
33
33
  ]
34
34
  }
35
35
 
36
- @query('#dialog') dialog!: HTMLElement & { open: boolean }
36
+ @query('#dialog') dialog!: HTMLElement & { show: () => void }
37
37
  @query('#subject-input') subjectInput!: HTMLInputElement
38
38
  @query('#sender-input') senderInput!: HTMLInputElement
39
39
  @query('#content-input') contentInput!: HTMLInputElement
40
40
 
41
41
  render() {
42
42
  return html`
43
- <md-elevated-button @click=${e => (this.dialog.open = true)}>${i18next.t('button.need help')}</md-elevated-button>
43
+ <md-elevated-button @click=${e => this.dialog.show()}>${i18next.t('button.need help')}</md-elevated-button>
44
44
 
45
45
  <md-dialog id="dialog" heading=${i18next.t('title.need help')}>
46
- <form action="" method="POST">
46
+ <form action="" method="post">
47
47
  <input id="subject-input" name="subject" type="hidden" />
48
48
  <input id="sender-input" name="sender" type="hidden" />
49
49
  <input id="content-input" name="content" type="hidden" />
50
50
  </form>
51
- <div id="input-form">
51
+
52
+ <div id="input-form" slot="content">
52
53
  <md-filled-text-field
53
54
  type="text"
54
55
  label=${i18next.t('label.subject')}
@@ -59,6 +60,7 @@ export class ContactUs extends localize(i18next)(LitElement) {
59
60
  this.subjectInput.value = val
60
61
  }}
61
62
  ></md-filled-text-field>
63
+
62
64
  <md-filled-text-field
63
65
  type="text"
64
66
  name="sender"
@@ -69,6 +71,7 @@ export class ContactUs extends localize(i18next)(LitElement) {
69
71
  this.senderInput.value = val
70
72
  }}
71
73
  ></md-filled-text-field>
74
+
72
75
  <md-filled-text-field
73
76
  name="content"
74
77
  type="textarea"
@@ -80,11 +83,11 @@ export class ContactUs extends localize(i18next)(LitElement) {
80
83
  this.contentInput.value = val
81
84
  }}
82
85
  ></md-filled-text-field>
86
+
87
+ <md-elevated-button slot="primaryAction" type="button" @click=${e => this._submit(e)}
88
+ >${i18next.t('button.submit')}</md-elevated-button
89
+ >
83
90
  </div>
84
- <md-elevated-button slot="primaryAction" type="submit" @click=${e => this._submit(e)}
85
- >${i18next.t('button.submit')}</md-elevated-button
86
- >
87
- <md-text-button slot="secondaryAction" dialogAction="cancel">${i18next.t('button.cancel')}</md-text-button>
88
91
  </md-dialog>
89
92
  `
90
93
  }
@@ -30,6 +30,7 @@ class CreateRole extends localize(i18next)(LitElement) {
30
30
 
31
31
  md-outlined-button {
32
32
  margin: var(--input-margin);
33
+ text-transform: capitalize;
33
34
  }
34
35
 
35
36
  @media screen and (max-width: 480px) {
@@ -6,6 +6,10 @@ import { customElement, query } from 'lit/decorators.js'
6
6
 
7
7
  import { i18next } from '@operato/i18n'
8
8
 
9
+ function capitalize(str) {
10
+ return str ? str.charAt(0).toUpperCase() + str.slice(1) : ''
11
+ }
12
+
9
13
  @customElement('create-user')
10
14
  class CreateUser extends LitElement {
11
15
  static styles = css`
@@ -18,7 +22,7 @@ class CreateUser extends LitElement {
18
22
  box-shadow: var(--box-shadow);
19
23
 
20
24
  display: grid;
21
- grid-template-columns: 1fr 2fr auto;
25
+ grid-template-columns: 1fr 2fr 2fr auto;
22
26
  gap: 5px 15px;
23
27
  clear: both;
24
28
  max-width: var(--input-container-max-width);
@@ -44,26 +48,42 @@ class CreateUser extends LitElement {
44
48
  `
45
49
 
46
50
  @query('[name=name]') nameInput!: HTMLInputElement
51
+ @query('[name=username]') usernameInput!: HTMLInputElement
47
52
  @query('[name=email]') emailInput!: HTMLInputElement
48
53
 
49
54
  render() {
50
55
  return html`
56
+ <md-filled-text-field
57
+ type="text"
58
+ name="username"
59
+ label=${capitalize(i18next.t('label.user-id'))}
60
+ pattern="^(?:[A-Za-z0-9]*|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,})$"
61
+ ><md-icon slot="leading-icon">badge</md-icon></md-filled-text-field
62
+ >
63
+
51
64
  <md-filled-text-field
52
65
  type="text"
53
66
  name="name"
54
- label=${String(i18next.t('label.x name', { x: i18next.t('label.user') }))}
55
- ></md-filled-text-field>
67
+ label=${capitalize(i18next.t('label.x name', { x: i18next.t('label.user') }))}
68
+ ><md-icon slot="leading-icon">id_card</md-icon></md-filled-text-field
69
+ >
56
70
 
57
- <md-filled-text-field type="email" name="email" label=${String(i18next.t('field.email'))}></md-filled-text-field>
71
+ <md-filled-text-field type="email" name="email" label=${capitalize(i18next.t('field.email'))}
72
+ ><md-icon slot="leading-icon">mail</md-icon></md-filled-text-field
73
+ >
58
74
 
59
75
  <md-outlined-button @click=${this.onCreateUser.bind(this)}
60
- >${String(i18next.t('button.create'))}</md-outlined-button
76
+ >${capitalize(i18next.t('button.create'))}</md-outlined-button
61
77
  >
62
78
  `
63
79
  }
64
80
 
65
81
  async onCreateUser() {
66
82
  try {
83
+ if (!this.usernameInput.value) {
84
+ throw new Error(i18next.t('error.value is empty', { value: 'name' }))
85
+ }
86
+
67
87
  if (!this.emailInput.checkValidity()) {
68
88
  throw new Error(i18next.t('error.not valid pattern of type', { type: 'e-mail' }))
69
89
  }
@@ -73,12 +93,14 @@ class CreateUser extends LitElement {
73
93
  }
74
94
 
75
95
  const user = {
96
+ username: this.usernameInput.value.trim(),
76
97
  name: this.nameInput.value.trim(),
77
98
  email: this.emailInput.value.trim()
78
99
  }
79
100
 
80
101
  await this.dispatchEvent(new CustomEvent('create-user', { detail: user }))
81
102
 
103
+ this.usernameInput.value = ''
82
104
  this.nameInput.value = ''
83
105
  this.emailInput.value = ''
84
106
  } catch (e: any) {
@@ -28,14 +28,23 @@ class InviteUser extends localize(i18next)(LitElement) {
28
28
 
29
29
  md-outlined-button {
30
30
  margin: var(--input-margin);
31
+ text-transform: capitalize;
31
32
  }
32
33
  `
33
34
 
34
- @query('input[name=email]') emailInput!: HTMLInputElement
35
+ @query('input[name=username]') userIdInput!: HTMLInputElement
35
36
 
36
37
  render() {
37
38
  return html`
38
- <input name="email" type="email" required name="invite-email" autocapitalize="off" />
39
+ <input
40
+ name="username"
41
+ type="text"
42
+ required
43
+ name="username"
44
+ autocapitalize="off"
45
+ placeholder=${String(i18next.t('text.user invitation prompt'))}
46
+ pattern="^(?:[A-Za-z0-9]*|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,})$"
47
+ />
39
48
  <md-outlined-button @click=${this.invite.bind(this)}>
40
49
  <md-icon slot="icon">group_add</md-icon>${String(i18next.t('label.invite user'))}
41
50
  </md-outlined-button>
@@ -44,8 +53,8 @@ class InviteUser extends localize(i18next)(LitElement) {
44
53
 
45
54
  async invite() {
46
55
  try {
47
- if (!this.emailInput.checkValidity()) {
48
- throw new Error(i18next.t('error.not valid pattern of type', { type: 'e-mail' }))
56
+ if (!this.userIdInput.checkValidity()) {
57
+ throw new Error(i18next.t('error.not valid pattern of type', { type: 'user-id or email' }))
49
58
  }
50
59
 
51
60
  if (
@@ -56,7 +65,7 @@ class InviteUser extends localize(i18next)(LitElement) {
56
65
  cancelButton: { text: i18next.t('button.cancel') }
57
66
  })
58
67
  ) {
59
- await this.inviteUser(this.emailInput.value)
68
+ await this.inviteUser(this.userIdInput.value)
60
69
 
61
70
  this.dispatchEvent(new CustomEvent('invitationCompleted'))
62
71
  }
@@ -72,14 +81,14 @@ class InviteUser extends localize(i18next)(LitElement) {
72
81
  }
73
82
  }
74
83
 
75
- async inviteUser(email) {
84
+ async inviteUser(username) {
76
85
  const response = await client.mutate({
77
86
  mutation: gql`
78
- mutation inviteUser($email: EmailAddress!) {
79
- inviteUser(email: $email)
87
+ mutation inviteUser($username: String!) {
88
+ inviteUser(username: $username)
80
89
  }
81
90
  `,
82
- variables: { email },
91
+ variables: { username },
83
92
  context: gqlContext()
84
93
  })
85
94
 
@@ -89,7 +98,7 @@ class InviteUser extends localize(i18next)(LitElement) {
89
98
  confirmButton: { text: i18next.t('button.confirm') }
90
99
  })
91
100
 
92
- this.emailInput.value = ''
101
+ this.userIdInput.value = ''
93
102
  }
94
103
  }
95
104
  }
@@ -75,11 +75,11 @@ class OwnershipTransferPopup extends localize(i18next)(LitElement) {
75
75
  ) {
76
76
  const response = await client.mutate({
77
77
  mutation: gql`
78
- mutation transferOwner($email: EmailAddress!) {
79
- transferOwner(email: $email)
78
+ mutation transferOwner($username: String!) {
79
+ transferOwner(username: $username)
80
80
  }
81
81
  `,
82
- variables: { email: this.user.email },
82
+ variables: { username: this.user.username || this.user.email },
83
83
  context: gqlContext()
84
84
  })
85
85
 
@@ -88,6 +88,10 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
88
88
  margin: var(--change-password-field-margin);
89
89
  }
90
90
 
91
+ md-text-button {
92
+ text-transform: capitalize;
93
+ }
94
+
91
95
  footer {
92
96
  padding: 20px;
93
97
  text-align: center;
@@ -107,7 +111,7 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
107
111
  `
108
112
  ]
109
113
 
110
- @property({ type: String }) userId?: string
114
+ @property({ type: String }) username?: string
111
115
  @property({ type: String }) email?: string
112
116
  @property({ type: String }) name?: string
113
117
 
@@ -124,7 +128,9 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
124
128
  looseCharacterLength?: number
125
129
  } = {}
126
130
 
131
+ @query('#username') usernameEl!: HTMLInputElement
127
132
  @query('#name') nameEl!: HTMLInputElement
133
+ @query('#email') emailEl!: HTMLInputElement
128
134
  @query('#locale') localeEl!: HTMLInputElement
129
135
 
130
136
  async connectedCallback(): Promise<void> {
@@ -163,11 +169,11 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
163
169
 
164
170
  setCredential(credential) {
165
171
  if (credential) {
166
- this.userId = credential.id
172
+ this.username = credential.username
167
173
  this.name = credential.name
168
174
  this.email = credential.email
169
175
  } else {
170
- this.userId = ''
176
+ this.username = ''
171
177
  this.name = ''
172
178
  this.email = ''
173
179
  }
@@ -176,13 +182,23 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
176
182
  render() {
177
183
  return html`
178
184
  <div class="wrap">
179
- <div class="user">${this.email || ''}</div>
185
+ <div class="user">${this.username || ''}</div>
186
+
187
+ <label for="username"><ox-i18n slot="title" msgid="label.user-id"></ox-i18n></label>
188
+ <input id="username" @change=${e => this.onUsernameChanged(e.target.value)} .value=${this.username || ''} />
189
+
190
+ <hr />
180
191
 
181
192
  <label for="name"><ox-i18n slot="title" msgid="label.name"></ox-i18n></label>
182
193
  <input id="name" @change=${e => this.onNameChanged(e.target.value)} .value=${this.name || ''} />
183
194
 
184
195
  <hr />
185
196
 
197
+ <label for="email"><ox-i18n slot="title" msgid="label.email"></ox-i18n></label>
198
+ <input id="email" type="email" @change=${e => this.onEmailChanged(e.target.value)} .value=${this.email || ''} />
199
+
200
+ <hr />
201
+
186
202
  <label for="locale"><ox-i18n slot="title" msgid="label.language"></ox-i18n></label>
187
203
  <ox-i18n-selector
188
204
  id="locale"
@@ -219,6 +235,30 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
219
235
  `
220
236
  }
221
237
 
238
+ async onUsernameChanged(username) {
239
+ if (!username) return
240
+
241
+ var oldUsername = this.username
242
+
243
+ try {
244
+ const message = await auth.updateProfile({
245
+ username
246
+ })
247
+
248
+ notify({
249
+ level: 'info',
250
+ message
251
+ })
252
+ } catch (e: any) {
253
+ this.usernameEl.value = oldUsername || ''
254
+
255
+ notify({
256
+ level: 'error',
257
+ message: 'message' in e ? e.message : e
258
+ })
259
+ }
260
+ }
261
+
222
262
  async onNameChanged(name) {
223
263
  if (!name) return
224
264
 
@@ -243,6 +283,30 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
243
283
  }
244
284
  }
245
285
 
286
+ async onEmailChanged(email) {
287
+ if (!email) return
288
+
289
+ var oldEmail = this.email
290
+
291
+ try {
292
+ const message = await auth.updateProfile({
293
+ email
294
+ })
295
+
296
+ notify({
297
+ level: 'info',
298
+ message
299
+ })
300
+ } catch (e: any) {
301
+ this.emailEl.value = oldEmail || ''
302
+
303
+ notify({
304
+ level: 'error',
305
+ message: 'message' in e ? e.message : e
306
+ })
307
+ }
308
+ }
309
+
246
310
  async onLocaleChanged(value) {
247
311
  if (!value) return
248
312
 
@@ -1,6 +1,6 @@
1
1
  import gql from 'graphql-tag'
2
2
  import { css, html, LitElement } from 'lit'
3
- import { customElement, property } from 'lit/decorators.js'
3
+ import { customElement, property, state } from 'lit/decorators.js'
4
4
 
5
5
  import { client, gqlContext } from '@operato/graphql'
6
6
  import { i18next, localize } from '@operato/i18n'
@@ -18,18 +18,32 @@ class RolePrivilegeEditor extends localize(i18next)(LitElement) {
18
18
 
19
19
  border: 1px solid var(--md-sys-color-primary);
20
20
  font: normal 15px var(--theme-font);
21
- color: var(--md-sys-color-secondary);
22
21
  }
22
+
23
23
  div {
24
24
  margin: var(--spacing-medium);
25
25
  }
26
+
27
+ div[users] {
28
+ margin: 0;
29
+ background-color: var(--md-sys-color-surface-variant);
30
+ }
31
+
32
+ div[titler] {
33
+ color: var(--md-sys-color-secondary);
34
+ font: bold 1.2em var(--theme-font);
35
+ text-transform: capitalize;
36
+ }
37
+
26
38
  ul {
27
39
  flex: 1;
40
+ color: var(--md-sys-color-secondary);
28
41
  background-color: var(--md-sys-color-surface-variant);
29
42
  overflow: auto;
30
43
  display: grid;
31
44
  grid-template-columns: 1fr 1fr;
32
- margin: 0;
45
+ margin-block-start: 0;
46
+ margin-block-end: 0;
33
47
  padding: var(--spacing-medium);
34
48
  list-style: none;
35
49
  border: 1px dashed rgba(0, 0, 0, 0.1);
@@ -70,14 +84,23 @@ class RolePrivilegeEditor extends localize(i18next)(LitElement) {
70
84
 
71
85
  @property({ type: Object }) role: any
72
86
  @property({ type: Array }) allPrivileges: any[] = []
73
- @property({ type: Array }) privileges: any[] = []
74
87
  @property({ type: Object }) updateRoleObj: any
75
88
 
89
+ @state() privileges: { id: string; description: string }[] = []
90
+ @state() users: { /* id: string; username: string; */ name: string; email: string }[] = []
91
+
76
92
  render() {
77
93
  const allPrivileges = this.allPrivileges || []
78
94
  const privileges = this.privileges || []
95
+ const users = this.users || []
79
96
 
80
97
  return html`
98
+ <div titler>${String(i18next.t('label.user'))}</div>
99
+
100
+ <div users>${users.map(user => html`<div>${user.name} (${user.email})</div>`)}</div>
101
+
102
+ <div titler>${String(i18next.t('field.privileges'))}</div>
103
+
81
104
  <div>
82
105
  <input id="checkAll" type="checkbox" @change=${e => this.oncheckAll(e)} />
83
106
  <label for="checkAll">Check all</label>
@@ -114,7 +137,7 @@ class RolePrivilegeEditor extends localize(i18next)(LitElement) {
114
137
 
115
138
  async updated(changes) {
116
139
  if (changes.has('role')) {
117
- this.privileges = await this.fetchPrivilegesOnRole(this.role.name)
140
+ await this.fetchPrivilegesOnRole(this.role.name)
118
141
  }
119
142
  }
120
143
 
@@ -224,6 +247,10 @@ class RolePrivilegeEditor extends localize(i18next)(LitElement) {
224
247
  id
225
248
  description
226
249
  }
250
+ users {
251
+ name
252
+ email
253
+ }
227
254
  }
228
255
  }
229
256
  `,
@@ -231,7 +258,8 @@ class RolePrivilegeEditor extends localize(i18next)(LitElement) {
231
258
  context: gqlContext()
232
259
  })
233
260
 
234
- return response.data.role?.privileges
261
+ this.privileges = response.data.role?.privileges
262
+ this.users = response.data.role?.users
235
263
  }
236
264
 
237
265
  showToast(message) {