@things-factory/auth-ui 8.0.0-beta.9 → 8.0.2

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 (100) hide show
  1. package/client/auth-style-sign.ts +194 -0
  2. package/client/bootstrap.ts +51 -0
  3. package/client/components/abstract-auth-page.ts +301 -0
  4. package/client/components/abstract-password-reset.ts +168 -0
  5. package/client/components/abstract-sign.ts +127 -0
  6. package/client/components/change-password.ts +153 -0
  7. package/client/components/contact-us.ts +113 -0
  8. package/client/components/create-domain-popup.ts +141 -0
  9. package/client/components/create-role.ts +123 -0
  10. package/client/components/create-user.ts +95 -0
  11. package/client/components/credential-manager.ts +64 -0
  12. package/client/components/delete-user-popup.ts +117 -0
  13. package/client/components/domain-switch.ts +127 -0
  14. package/client/components/invite-customer.ts +104 -0
  15. package/client/components/invite-user.ts +96 -0
  16. package/client/components/my-login-history.ts +101 -0
  17. package/client/components/ownership-transfer-popup.ts +110 -0
  18. package/client/components/partner-info-card.ts +89 -0
  19. package/client/components/partner-role-editor.ts +153 -0
  20. package/client/components/profile-component.ts +332 -0
  21. package/client/components/role-edit-form.ts +92 -0
  22. package/client/components/role-privilege-editor.ts +267 -0
  23. package/client/components/role-selector.ts +102 -0
  24. package/client/components/user-role-editor.ts +499 -0
  25. package/client/constants/application.ts +9 -0
  26. package/client/constants/index.ts +1 -0
  27. package/client/entries/auth/activate.ts +272 -0
  28. package/client/entries/auth/checkin.ts +190 -0
  29. package/client/entries/auth/forgot-password.ts +103 -0
  30. package/client/entries/auth/reset-password.ts +22 -0
  31. package/client/entries/auth/result.ts +193 -0
  32. package/client/entries/auth/signin.ts +18 -0
  33. package/client/entries/auth/signup.ts +109 -0
  34. package/client/entries/auth/unlock-user.ts +22 -0
  35. package/client/entries/oauth2/oauth2-decision-error-page.ts +50 -0
  36. package/client/entries/oauth2/oauth2-decision-page.ts +196 -0
  37. package/client/entries/public/home.ts +246 -0
  38. package/client/index.ts +124 -0
  39. package/client/pages/app-binding/app-binding.ts +423 -0
  40. package/client/pages/app-binding/app-bindings.ts +171 -0
  41. package/client/pages/appliance/appliance.ts +452 -0
  42. package/client/pages/appliance/home.ts +177 -0
  43. package/client/pages/appliance/register.ts +183 -0
  44. package/client/pages/application/application.ts +428 -0
  45. package/client/pages/application/applications.ts +182 -0
  46. package/client/pages/application/register.ts +211 -0
  47. package/client/pages/attribute/attribute-set-item-list.ts +237 -0
  48. package/client/pages/attribute/attribute-set-management.ts +282 -0
  49. package/client/pages/auth-provider/auth-provider-management.ts +381 -0
  50. package/client/pages/domain/domain-management.ts +410 -0
  51. package/client/pages/partner/partner-management.ts +112 -0
  52. package/client/pages/profile.ts +32 -0
  53. package/client/pages/role/role-management.ts +134 -0
  54. package/client/pages/user/user-management.ts +224 -0
  55. package/client/route.ts +67 -0
  56. package/client/themes/auth-theme.css +65 -0
  57. package/client/utils/password-rule.ts +37 -0
  58. package/dist-client/components/abstract-auth-page.js +10 -10
  59. package/dist-client/components/abstract-auth-page.js.map +1 -1
  60. package/dist-client/components/abstract-password-reset.d.ts +2 -1
  61. package/dist-client/components/abstract-password-reset.js +14 -7
  62. package/dist-client/components/abstract-password-reset.js.map +1 -1
  63. package/dist-client/components/abstract-sign.js +11 -12
  64. package/dist-client/components/abstract-sign.js.map +1 -1
  65. package/dist-client/components/contact-us.d.ts +1 -1
  66. package/dist-client/components/contact-us.js +7 -10
  67. package/dist-client/components/contact-us.js.map +1 -1
  68. package/dist-client/components/create-user.js +5 -28
  69. package/dist-client/components/create-user.js.map +1 -1
  70. package/dist-client/components/invite-user.js +11 -19
  71. package/dist-client/components/invite-user.js.map +1 -1
  72. package/dist-client/components/ownership-transfer-popup.js +3 -3
  73. package/dist-client/components/ownership-transfer-popup.js.map +1 -1
  74. package/dist-client/components/profile-component.d.ts +1 -5
  75. package/dist-client/components/profile-component.js +4 -64
  76. package/dist-client/components/profile-component.js.map +1 -1
  77. package/dist-client/components/role-privilege-editor.js +1 -2
  78. package/dist-client/components/role-privilege-editor.js.map +1 -1
  79. package/dist-client/components/user-role-editor.js +18 -18
  80. package/dist-client/components/user-role-editor.js.map +1 -1
  81. package/dist-client/entries/auth/checkin.js +1 -1
  82. package/dist-client/entries/auth/checkin.js.map +1 -1
  83. package/dist-client/entries/auth/forgot-password.js +2 -11
  84. package/dist-client/entries/auth/forgot-password.js.map +1 -1
  85. package/dist-client/entries/auth/signup.js +7 -13
  86. package/dist-client/entries/auth/signup.js.map +1 -1
  87. package/dist-client/index.js +1 -1
  88. package/dist-client/index.js.map +1 -1
  89. package/dist-client/pages/user/user-management.d.ts +1 -5
  90. package/dist-client/pages/user/user-management.js +7 -6
  91. package/dist-client/pages/user/user-management.js.map +1 -1
  92. package/dist-client/tsconfig.tsbuildinfo +1 -1
  93. package/dist-server/tsconfig.tsbuildinfo +1 -1
  94. package/package.json +12 -12
  95. package/server/index.ts +0 -0
  96. package/translations/en.json +2 -6
  97. package/translations/ja.json +2 -6
  98. package/translations/ko.json +2 -6
  99. package/translations/ms.json +2 -6
  100. package/translations/zh.json +2 -6
@@ -0,0 +1,127 @@
1
+ import '@operato/i18n/ox-i18n.js'
2
+
3
+ import { html, nothing } from 'lit'
4
+ import { startAuthentication } from '@simplewebauthn/browser'
5
+
6
+ import { i18next } from '@operato/i18n'
7
+ import { notify } from '@operato/layout'
8
+
9
+ import { AbstractAuthPage } from './abstract-auth-page.js'
10
+
11
+ const isAvailableWebauthn = 'PublicKeyCredential' in window
12
+
13
+ interface AssertionResponse {
14
+ id: string
15
+ rawId?: number[]
16
+ response: {
17
+ authenticatorData: string
18
+ clientDataJSON: string
19
+ signature: string
20
+ userHandle: string | null
21
+ }
22
+ type: PublicKeyCredentialType
23
+ authenticatorAttachment?: AuthenticatorAttachment
24
+ }
25
+
26
+ export abstract class AbstractSign extends AbstractAuthPage {
27
+ async submit() {
28
+ this.formEl.submit()
29
+ }
30
+
31
+ updated(changed) {
32
+ super.updated(changed)
33
+
34
+ if (changed.has('message')) {
35
+ if (!this.message) {
36
+ this.hideSnackbar()
37
+ } else {
38
+ this.showSnackbar({
39
+ level: 'error',
40
+ timer: -1
41
+ })
42
+ }
43
+ }
44
+ }
45
+
46
+ get formfields() {
47
+ const email = this.data?.email || ''
48
+ const autocompletable = this.autocompletable
49
+
50
+ return html`
51
+ <input id="redirectTo" type="hidden" name="redirectTo" .value=${this.redirectTo || '/'} />
52
+
53
+ <div class="field">
54
+ <md-filled-text-field
55
+ name="email"
56
+ type="email"
57
+ label=${String(i18next.t('field.email'))}
58
+ required
59
+ .value=${email}
60
+ autocomplete=${autocompletable ? "username" : "off"}
61
+ autocapitalize="off"
62
+ @input=${(e: Event) => {
63
+ const target = e.target as HTMLInputElement
64
+ if (target.validity.typeMismatch) {
65
+ target.setCustomValidity(i18next.t('text.invalid-email'))
66
+ } else {
67
+ target.setCustomValidity('')
68
+ }
69
+ }}
70
+ ><md-icon slot="leading-icon">mail</md-icon></md-filled-text-field
71
+ >
72
+ </div>
73
+ <div class="field">
74
+ <md-filled-text-field
75
+ name="password"
76
+ type="password"
77
+ label=${String(i18next.t('field.password'))}
78
+ autocomplete=${autocompletable ? "current-password" : "off"}
79
+ required
80
+ ><md-icon slot="leading-icon">vpn_key</md-icon></md-filled-text-field
81
+ >
82
+ </div>
83
+
84
+ <div class="submit-buttons-container">
85
+ <md-elevated-button class="submit-button" type="submit" raised @click=${e => this._onSubmit(e)}>
86
+ <ox-i18n msgid="field.${this.pageName}"> </ox-i18n>
87
+ </md-elevated-button>
88
+ ${isAvailableWebauthn
89
+ ? html` <md-icon class="fingerprint" raised @click=${e => this.authenticateUser()}> fingerprint </md-icon>`
90
+ : nothing}
91
+ </div>
92
+ `
93
+ }
94
+
95
+ async authenticateUser() {
96
+ try {
97
+ const options = await fetch('/auth/signin-webauthn/challenge').then(res => res.json())
98
+ const assertionResp = await startAuthentication(options)
99
+ const verification = await fetch('/auth/signin-webauthn', {
100
+ method: 'POST',
101
+ headers: {
102
+ 'Content-Type': 'application/json'
103
+ },
104
+ body: JSON.stringify(assertionResp)
105
+ }).then(res => res.json())
106
+
107
+ if (verification.verified) {
108
+ const { redirectURL } = verification
109
+
110
+ if (redirectURL) {
111
+ window.location.href = redirectURL
112
+ }
113
+ } else {
114
+ notify({
115
+ level: 'error',
116
+ message: verification.message
117
+ })
118
+ }
119
+
120
+ } catch (error) {
121
+ notify({
122
+ level: 'error',
123
+ message: i18next.t('error.authn verification failed')
124
+ })
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,153 @@
1
+ import { css, html, LitElement, PropertyValues } from 'lit'
2
+ import { customElement, property, query } from 'lit/decorators.js'
3
+
4
+ import { i18next, localize } from '@operato/i18n'
5
+ import { auth } from '@things-factory/auth-base/dist-client/auth.js'
6
+ import { generatePasswordPatternHelp, generatePasswordPatternRegExp } from '../utils/password-rule'
7
+
8
+ @customElement('change-password')
9
+ export class ChangePassword extends localize(i18next)(LitElement) {
10
+ static styles = [
11
+ css`
12
+ * {
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ *:focus {
17
+ outline: none;
18
+ }
19
+
20
+ form {
21
+ display: flex;
22
+ flex-direction: column;
23
+ }
24
+
25
+ input {
26
+ border: var(--change-password-field-border);
27
+ border-radius: var(--change-password-field-border-radius);
28
+ margin: var(--change-password-field-margin);
29
+ padding: var(--change-password-field-padding);
30
+
31
+ font: var(--change-password-field-font);
32
+ width: var(--change-password-field-width);
33
+ }
34
+
35
+ input:focus {
36
+ border: 1px solid var(--focus-background-color);
37
+ }
38
+
39
+ ::placeholder {
40
+ font-size: 0.8rem;
41
+ text-transform: capitalize;
42
+ }
43
+
44
+ md-elevated-button {
45
+ margin: var(--spacing-small) auto var(--spacing-medium) auto;
46
+ text-transform: capitalize;
47
+ }
48
+
49
+ button {
50
+ background-color: var(--secondary-color, #394e64);
51
+ margin: 2px 2px 10px 2px;
52
+ height: var(--button-height, 28px);
53
+ color: var(--button-color, #fff);
54
+ font: var(--button-font);
55
+ border-radius: var(--button-radius, 5px);
56
+ border: var(--button-border, 1px solid transparent);
57
+ line-height: 1.5;
58
+ }
59
+
60
+ button:hover,
61
+ button:active {
62
+ background-color: var(--button-active-background-color, #22a6a7);
63
+ border: var(--button-active-border);
64
+ }
65
+
66
+ .helper-text {
67
+ font-size: 12px;
68
+ color: var(--md-sys-color-) #6c757d;
69
+ margin-top: 4px;
70
+ display: block; /* 텍스트를 입력 필드 아래에 배치 */
71
+ line-height: 1.5; /* 텍스트 줄 간격 조절 */
72
+ }
73
+ `
74
+ ]
75
+
76
+ @property({ type: Object }) passwordRule: {
77
+ lowerCase?: boolean
78
+ upperCase?: boolean
79
+ digit?: boolean
80
+ specialCharacter?: boolean
81
+ allowRepeat?: boolean
82
+ useTightPattern?: boolean
83
+ useLoosePattern?: boolean
84
+ tightCharacterLength?: number
85
+ looseCharacterLength?: number
86
+ } = {}
87
+
88
+ @query('form') form!: HTMLFormElement
89
+
90
+ private passwordPattern: string = ''
91
+ private passwordHelp: string = ''
92
+
93
+ render() {
94
+ return html`
95
+ <form>
96
+ <div class="field">
97
+ <input type="password" name="current_pass" placeholder=${i18next.t('text.current password')} required />
98
+ </div>
99
+ <span id="password-helper" class="helper-text">${this.passwordHelp}</span>
100
+ <div class="field">
101
+ <input
102
+ type="password"
103
+ name="new_pass"
104
+ placeholder=${i18next.t('text.new password')}
105
+ required
106
+ pattern=${this.passwordPattern}
107
+ aria-describedby="password-helper"
108
+ />
109
+ </div>
110
+ <div class="field">
111
+ <input type="password" name="confirm_pass" placeholder=${i18next.t('text.confirm password')} required />
112
+ </div>
113
+
114
+ <md-elevated-button @click=${this.submit.bind(this)}>${i18next.t('text.change password')}</md-elevated-button>
115
+ </form>
116
+ `
117
+ }
118
+
119
+ updated(changes: PropertyValues<this>) {
120
+ if (changes.has('passwordRule')) {
121
+ this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source
122
+ this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)
123
+ }
124
+ }
125
+
126
+ languageUpdated() {
127
+ this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source
128
+ this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)
129
+ }
130
+
131
+ async submit() {
132
+ const formData = new FormData(this.form)
133
+ let params = {}
134
+ for (const [key, value] of formData.entries()) {
135
+ if (!value) {
136
+ const placeholder = (this.form.querySelector(`[name=${key}]`) as HTMLInputElement)?.placeholder
137
+ return this.showToast(i18next.t('error.value is empty', { value: placeholder || key }))
138
+ }
139
+ params[key] = value
140
+ }
141
+
142
+ if (params['new_pass'] !== params['confirm_pass']) {
143
+ return this.showToast(i18next.t('error.new-password-and-confirm-password-do-not-match'))
144
+ }
145
+
146
+ auth.changePassword(params)
147
+ this.form.reset()
148
+ }
149
+
150
+ showToast(message) {
151
+ document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))
152
+ }
153
+ }
@@ -0,0 +1,113 @@
1
+ import '@material/web/button/text-button.js'
2
+ import '@material/web/button/elevated-button.js'
3
+ import '@material/web/textfield/filled-text-field.js'
4
+ import '@material/web/dialog/dialog.js'
5
+
6
+ import '@operato/i18n'
7
+
8
+ import { css, html, LitElement } from 'lit'
9
+ import { customElement, query } from 'lit/decorators.js'
10
+
11
+ import { i18next, localize } from '@operato/i18n'
12
+ import { auth } from '@things-factory/auth-base/dist-client/auth.js'
13
+
14
+ @customElement('contact-us')
15
+ export class ContactUs extends localize(i18next)(LitElement) {
16
+ static get styles() {
17
+ return [
18
+ css`
19
+ * {
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ *:focus {
24
+ outline: none;
25
+ }
26
+
27
+ #input-form {
28
+ display: grid;
29
+ grid-template-rows: 1fr 1fr 3fr;
30
+ grid-gap: 10px 0;
31
+ }
32
+ `
33
+ ]
34
+ }
35
+
36
+ @query('#dialog') dialog!: HTMLElement & { open: boolean }
37
+ @query('#subject-input') subjectInput!: HTMLInputElement
38
+ @query('#sender-input') senderInput!: HTMLInputElement
39
+ @query('#content-input') contentInput!: HTMLInputElement
40
+
41
+ render() {
42
+ return html`
43
+ <md-elevated-button @click=${e => (this.dialog.open = true)}>${i18next.t('button.need help')}</md-elevated-button>
44
+
45
+ <md-dialog id="dialog" heading=${i18next.t('title.need help')}>
46
+ <form action="" method="POST">
47
+ <input id="subject-input" name="subject" type="hidden" />
48
+ <input id="sender-input" name="sender" type="hidden" />
49
+ <input id="content-input" name="content" type="hidden" />
50
+ </form>
51
+ <div id="input-form">
52
+ <md-filled-text-field
53
+ type="text"
54
+ label=${i18next.t('label.subject')}
55
+ dialogInitialFocus
56
+ required
57
+ @input=${e => {
58
+ const val = e.target.value
59
+ this.subjectInput.value = val
60
+ }}
61
+ ></md-filled-text-field>
62
+ <md-filled-text-field
63
+ type="text"
64
+ name="sender"
65
+ label=${i18next.t('label.email')}
66
+ required
67
+ @input=${e => {
68
+ const val = e.target.value
69
+ this.senderInput.value = val
70
+ }}
71
+ ></md-filled-text-field>
72
+ <md-filled-text-field
73
+ name="content"
74
+ type="textarea"
75
+ label=${i18next.t('label.content')}
76
+ required
77
+ @keydown=${e => e.stopPropagation()}
78
+ @input=${e => {
79
+ const val = e.target.value
80
+ this.contentInput.value = val
81
+ }}
82
+ ></md-filled-text-field>
83
+ </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
+ </md-dialog>
89
+ `
90
+ }
91
+
92
+ _checkValidity(): boolean {
93
+ return false
94
+ }
95
+
96
+ _submit(e: MouseEvent) {
97
+ if (!this._checkValidity()) return
98
+
99
+ const form = e.target as HTMLFormElement
100
+
101
+ const formData = new FormData(form)
102
+ let json = {}
103
+
104
+ //convert form into json
105
+ for (const [key, value] of formData.entries()) {
106
+ json[key] = value
107
+ }
108
+
109
+ auth.changePassword(json)
110
+
111
+ form.reset()
112
+ }
113
+ }
@@ -0,0 +1,141 @@
1
+ import '@material/web/icon/icon.js'
2
+
3
+ import gql from 'graphql-tag'
4
+ import { css, html, LitElement } from 'lit'
5
+ import { customElement, query } from 'lit/decorators.js'
6
+
7
+ import { client } from '@operato/graphql'
8
+ import { i18next, localize } from '@operato/i18n'
9
+ import { OxPrompt } from '@operato/popup/ox-prompt.js'
10
+ import { CommonHeaderStyles } from '@operato/styles'
11
+
12
+ @customElement('create-domain-popup')
13
+ class CreateDomainPopup extends localize(i18next)(LitElement) {
14
+ static styles = [
15
+ CommonHeaderStyles,
16
+ css`
17
+ :host {
18
+ display: flex;
19
+ flex-direction: column;
20
+ background-color: var(--md-sys-color-background);
21
+ overflow: auto;
22
+ }
23
+
24
+ form {
25
+ flex: 1;
26
+ padding: var(--spacing-large);
27
+ }
28
+
29
+ input.checkValidName {
30
+ background-color: #fce6e6;
31
+ }
32
+
33
+ label {
34
+ display: flex;
35
+ flex-direction: column;
36
+
37
+ font: var(--label-font);
38
+ color: var(--label-color, var(--md-sys-color-on-surface));
39
+ text-transform: var(--label-text-transform);
40
+ }
41
+
42
+ input {
43
+ border: var(--border-dim-color);
44
+ border-radius: var(--border-radius);
45
+ margin: var(--input-margin);
46
+ padding: var(--input-padding);
47
+ background-color: var(--md-sys-color-surface);
48
+ font: var(--input-font);
49
+ }
50
+
51
+ [field] {
52
+ grid-column: span 2;
53
+ }
54
+ `
55
+ ]
56
+
57
+ @query('input[name="name"]') nameInput!: HTMLInputElement
58
+
59
+ render() {
60
+ return html`
61
+ <form>
62
+ <div field grid-span>
63
+ <label
64
+ >${i18next.t('label.x name', { x: i18next.t('label.domain') })}<input
65
+ type="text"
66
+ name="name"
67
+ @input=${this.checkValidation}
68
+ autofocus
69
+ /></label>
70
+ </div>
71
+
72
+ <div field grid-span>
73
+ <label>${i18next.t('label.description')}<input type="text" name="description" /></label>
74
+ </div>
75
+ </form>
76
+
77
+ <div class="footer">
78
+ <div filler></div>
79
+ <button @click=${e => this.onCreateDomain()} done><md-icon>add</md-icon>${i18next.t('button.create')}</button>
80
+ </div>
81
+ `
82
+ }
83
+
84
+ firstUpdated() {
85
+ this.nameInput.focus() // autofocus
86
+ }
87
+
88
+ get inputData() {
89
+ return this.renderRoot.querySelectorAll('input')
90
+ }
91
+
92
+ checkValidation(e) {
93
+ const currentInput = e.currentTarget
94
+ const regExp = /^[a-zA-Z ]+$/
95
+
96
+ if (!regExp.test(currentInput.value)) {
97
+ currentInput.classList.add('checkValidName')
98
+ } else {
99
+ currentInput.classList.remove('checkValidName')
100
+ }
101
+ }
102
+
103
+ async onCreateDomain() {
104
+ const domainInput: { [prop: string]: string } = {}
105
+ this.inputData.forEach(data => (domainInput[data.name] = data.value))
106
+ const regExp = /^[a-zA-z0-9- ]+$/
107
+
108
+ if (!regExp.test(domainInput.name)) {
109
+ return this.showToast(i18next.t('error: domain name should consist only of letters or dashes'))
110
+ }
111
+
112
+ const response = await client.mutate({
113
+ mutation: gql`
114
+ mutation domainRegister($domainInput: DomainGeneratorInput!) {
115
+ domainRegister(domainInput: $domainInput) {
116
+ id
117
+ name
118
+ }
119
+ }
120
+ `,
121
+ variables: { domainInput }
122
+ })
123
+
124
+ if (!response.errors) {
125
+ await OxPrompt.open({
126
+ type: 'success',
127
+ title: i18next.t('text.completed'),
128
+ text: i18next.t('text.x_created_successfully', { x: i18next.t('label.domain') }),
129
+ confirmButton: { text: i18next.t('button.confirm') }
130
+ })
131
+
132
+ history.back()
133
+
134
+ this.dispatchEvent(new CustomEvent('fetch-data'))
135
+ }
136
+ }
137
+
138
+ showToast(message) {
139
+ document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))
140
+ }
141
+ }
@@ -0,0 +1,123 @@
1
+ import '@material/web/textfield/filled-text-field.js'
2
+
3
+ import gql from 'graphql-tag'
4
+ import { css, html, LitElement } from 'lit'
5
+ import { customElement, query } from 'lit/decorators.js'
6
+
7
+ import { client, gqlContext } from '@operato/graphql'
8
+ import { i18next, localize } from '@operato/i18n'
9
+ import { OxPrompt } from '@operato/popup/ox-prompt.js'
10
+
11
+ @customElement('create-role')
12
+ class CreateRole extends localize(i18next)(LitElement) {
13
+ static styles = css`
14
+ :host {
15
+ --md-text-field-fill-color: var(--md-sys-color-on-primary);
16
+ background-color: var(--md-sys-color-surface);
17
+ margin: var(--spacing-large) 0;
18
+ padding: var(--spacing-large);
19
+ border-radius: var(--border-radius);
20
+ box-shadow: var(--box-shadow);
21
+
22
+ display: grid;
23
+ grid-template-columns: 1fr 2fr auto;
24
+ gap: 5px 15px;
25
+ clear: both;
26
+ max-width: var(--input-container-max-width);
27
+
28
+ align-items: center;
29
+ }
30
+
31
+ md-outlined-button {
32
+ margin: var(--input-margin);
33
+ text-transform: capitalize;
34
+ }
35
+
36
+ @media screen and (max-width: 480px) {
37
+ :host {
38
+ grid-template-columns: 1fr 1fr;
39
+ }
40
+
41
+ md-outlined-button {
42
+ grid-column: span 2;
43
+
44
+ margin: var(--input-margin);
45
+ }
46
+ }
47
+ `
48
+
49
+ @query('[name=name]') nameInput!: HTMLInputElement
50
+ @query('[name=description]') descriptionInput!: HTMLInputElement
51
+
52
+ render() {
53
+ return html`
54
+ <md-filled-text-field
55
+ type="text"
56
+ name="name"
57
+ label=${String(i18next.t('label.x name', { x: i18next.t('label.role') }))}
58
+ ></md-filled-text-field>
59
+ <md-filled-text-field
60
+ type="text"
61
+ name="description"
62
+ label=${String(i18next.t('label.x description', { x: i18next.t('label.role') }))}
63
+ ></md-filled-text-field>
64
+
65
+ <md-outlined-button @click=${this.onCreateRole.bind(this)}
66
+ >${String(i18next.t('button.create'))}</md-outlined-button
67
+ >
68
+ `
69
+ }
70
+
71
+ async onCreateRole() {
72
+ let role: { name?: string; description?: string } = {}
73
+
74
+ const name = this.nameInput.value.trim()
75
+ const description = this.descriptionInput.value
76
+
77
+ if (!name) {
78
+ return this.showToast(i18next.t('error.value is empty', { value: i18next.t('field.name') }))
79
+ }
80
+
81
+ role.name = name
82
+ role.description = description
83
+
84
+ if (
85
+ await OxPrompt.open({
86
+ title: i18next.t('text.are_you_sure'),
87
+ text: i18next.t('text.do_you_want_to_create_x', { x: i18next.t('label.role') }),
88
+ confirmButton: { text: i18next.t('button.confirm') },
89
+ cancelButton: { text: i18next.t('button.cancel') }
90
+ })
91
+ ) {
92
+ const response = await client.mutate({
93
+ mutation: gql`
94
+ mutation createRole($role: NewRole!) {
95
+ createRole(role: $role) {
96
+ name
97
+ }
98
+ }
99
+ `,
100
+ variables: { role },
101
+ context: gqlContext()
102
+ })
103
+
104
+ if (!response.errors) {
105
+ await this.dispatchEvent(new CustomEvent('fetch-roles'))
106
+
107
+ await OxPrompt.open({
108
+ type: 'success',
109
+ title: i18next.t('text.completed'),
110
+ text: i18next.t('text.data_uploaded_successfully'),
111
+ confirmButton: { text: i18next.t('button.confirm') }
112
+ })
113
+
114
+ this.nameInput.value = ''
115
+ this.descriptionInput.value = ''
116
+ }
117
+ }
118
+ }
119
+
120
+ showToast(message) {
121
+ document.dispatchEvent(new CustomEvent('notify', { detail: { message, option: { timer: 1000 } } }))
122
+ }
123
+ }