@things-factory/auth-ui 10.1.3 → 10.1.6

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 (38) hide show
  1. package/dist-client/components/change-password.d.ts +11 -0
  2. package/dist-client/components/change-password.js +103 -6
  3. package/dist-client/components/change-password.js.map +1 -1
  4. package/dist-client/components/role-detail.d.ts +144 -0
  5. package/dist-client/components/role-detail.js +698 -0
  6. package/dist-client/components/role-detail.js.map +1 -0
  7. package/dist-client/components/user-detail.d.ts +151 -0
  8. package/dist-client/components/user-detail.js +631 -0
  9. package/dist-client/components/user-detail.js.map +1 -0
  10. package/dist-client/components/user-invite.d.ts +80 -0
  11. package/dist-client/components/user-invite.js +422 -0
  12. package/dist-client/components/user-invite.js.map +1 -0
  13. package/dist-client/entries/auth/activate.d.ts +0 -1
  14. package/dist-client/entries/auth/activate.js +0 -8
  15. package/dist-client/entries/auth/activate.js.map +1 -1
  16. package/dist-client/pages/role/role-list-page.d.ts +112 -0
  17. package/dist-client/pages/role/role-list-page.js +514 -0
  18. package/dist-client/pages/role/role-list-page.js.map +1 -0
  19. package/dist-client/pages/role/role-privilege-page.d.ts +123 -0
  20. package/dist-client/pages/role/role-privilege-page.js +801 -0
  21. package/dist-client/pages/role/role-privilege-page.js.map +1 -0
  22. package/dist-client/pages/user/user-list-page.d.ts +88 -0
  23. package/dist-client/pages/user/user-list-page.js +474 -0
  24. package/dist-client/pages/user/user-list-page.js.map +1 -0
  25. package/dist-client/route.js +5 -2
  26. package/dist-client/route.js.map +1 -1
  27. package/dist-client/tsconfig.tsbuildinfo +1 -1
  28. package/dist-server/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +4 -4
  30. package/things-factory.config.js +3 -2
  31. package/translations/en.json +46 -3
  32. package/translations/ja.json +0 -2
  33. package/translations/ko.json +47 -4
  34. package/translations/ms.json +0 -2
  35. package/translations/zh.json +0 -2
  36. package/dist-client/components/contact-us.d.ts +0 -20
  37. package/dist-client/components/contact-us.js +0 -121
  38. package/dist-client/components/contact-us.js.map +0 -1
@@ -14,6 +14,17 @@ export declare class ChangePassword extends ChangePassword_base {
14
14
  looseCharacterLength?: number;
15
15
  };
16
16
  form: HTMLFormElement;
17
+ /**
18
+ * Set once the change went through.
19
+ *
20
+ * The server answers with a message and the shell shows it as a toast, but a toast leaves and
21
+ * the same three empty boxes stay behind. A person who has just changed their password reads
22
+ * that as "nothing happened" — the report that brought this in said exactly that.
23
+ *
24
+ * So the form is replaced by a statement that stays. It also stops the boxes from inviting a
25
+ * second attempt at something already done.
26
+ */
27
+ private changed;
17
28
  private passwordPattern;
18
29
  private passwordHelp;
19
30
  render(): import("lit-html").TemplateResult<1>;
@@ -1,6 +1,6 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import { css, html, LitElement } from 'lit';
3
- import { customElement, property, query } from 'lit/decorators.js';
3
+ import { customElement, property, query, state } from 'lit/decorators.js';
4
4
  import { i18next, localize } from '@operato/i18n';
5
5
  import { FormFieldStyles } from '@operato/styles';
6
6
  import { auth } from '@things-factory/auth-base/dist-client/auth.js';
@@ -9,6 +9,17 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
9
9
  constructor() {
10
10
  super(...arguments);
11
11
  this.passwordRule = {};
12
+ /**
13
+ * Set once the change went through.
14
+ *
15
+ * The server answers with a message and the shell shows it as a toast, but a toast leaves and
16
+ * the same three empty boxes stay behind. A person who has just changed their password reads
17
+ * that as "nothing happened" — the report that brought this in said exactly that.
18
+ *
19
+ * So the form is replaced by a statement that stays. It also stops the boxes from inviting a
20
+ * second attempt at something already done.
21
+ */
22
+ this.changed = false;
12
23
  this.passwordPattern = '';
13
24
  this.passwordHelp = '';
14
25
  }
@@ -73,6 +84,40 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
73
84
  }
74
85
 
75
86
  /* 비밀번호 규칙 안내문 — 본문보다 낮은 강조. 라이트/다크 모두 읽히도록 토큰 파생. */
87
+ /* The statement that replaces the form once the change took. */
88
+ .changed {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 8px;
92
+ /*
93
+ * 같은 여백을 위쪽 인풋과 나눠 쓴다. 폼 상태에서는 첫 인풋의 margin 이 「비밀번호」
94
+ * 라벨과의 간격을 만드는데, 이 문장은 margin 이 없어 라벨에 붙어 있었다 — 같은 자리에
95
+ * 두 가지 간격이 생겼다. 값을 새로 정하지 않고 인풋과 같은 토큰을 쓴다.
96
+ */
97
+ margin: var(--change-password-field-margin, var(--spacing-small) 0);
98
+ padding: 12px 14px;
99
+ border: 1px solid var(--md-sys-color-outline-variant);
100
+ border-left: 3px solid var(--md-sys-color-primary);
101
+ border-radius: 7px;
102
+ background: var(--md-sys-color-surface-variant);
103
+ color: var(--md-sys-color-on-surface-variant);
104
+ line-height: 1.5;
105
+ }
106
+
107
+ .changed md-icon {
108
+ color: var(--md-sys-color-primary);
109
+ }
110
+
111
+ .changed button {
112
+ margin-left: auto;
113
+ border: 1px solid var(--md-sys-color-outline-variant);
114
+ background: transparent;
115
+ color: inherit;
116
+ border-radius: 7px;
117
+ padding: 5px 11px;
118
+ cursor: pointer;
119
+ }
120
+
76
121
  .helper-text {
77
122
  font-size: 12px;
78
123
  color: var(--md-sys-color-on-surface-variant);
@@ -83,12 +128,32 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
83
128
  `
84
129
  ]; }
85
130
  render() {
131
+ if (this.changed) {
132
+ return html `
133
+ <div class="changed" role="status">
134
+ <md-icon>check_circle</md-icon>
135
+ <span>${i18next.t('text.password changed successfully')}</span>
136
+ <button type="button" @click=${() => (this.changed = false)}>
137
+ ${i18next.t('text.change password')}
138
+ </button>
139
+ </div>
140
+ `;
141
+ }
86
142
  return html `
87
143
  <form>
88
144
  <div class="field">
89
145
  <input type="password" name="current_pass" placeholder=${i18next.t('text.current password')} required />
90
146
  </div>
91
- <span id="password-helper" class="helper-text">${this.passwordHelp}</span>
147
+ <!--
148
+ The rule belongs under the field the rule applies to.
149
+
150
+ It used to sit between the two, directly beneath the current password, so a sighted
151
+ reader took it as the rule for that field — and the current password has no reason to
152
+ satisfy today's rule. It may predate it. The server only checks that it matches
153
+ (change-pwd.ts verifies the current one and validates the new one against the rule).
154
+ aria-describedby always pointed at the new field, so a screen reader had it right and
155
+ only the eye was misled.
156
+ -->
92
157
  <div class="field">
93
158
  <input
94
159
  type="password"
@@ -99,11 +164,25 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
99
164
  aria-describedby="password-helper"
100
165
  />
101
166
  </div>
167
+ <span id="password-helper" class="helper-text">${this.passwordHelp}</span>
102
168
  <div class="field">
103
169
  <input type="password" name="confirm_pass" placeholder=${i18next.t('text.confirm password')} required />
104
170
  </div>
105
171
 
106
- <md-elevated-button @click=${this.submit.bind(this)}>${i18next.t('text.change password')}</md-elevated-button>
172
+ <!--
173
+ type="button", the way abstract-sign and abstract-auth-page already declare it.
174
+
175
+ A Material button inside a form defaults to type="submit" and submits unless the click
176
+ is default-prevented. So one click ran submit() and then let the browser submit the
177
+ form as well. submit() had already called form.reset(), so constraint validation found
178
+ three empty required fields, rejected the first one, and put the caret back on the
179
+ current password with an English "Please fill out this field." bubble on a Korean
180
+ screen. It read as the current password being refused.
181
+ [확인함] 2026-09-09 · 사용자 보고 · 화면 사진
182
+ -->
183
+ <md-elevated-button type="button" @click=${this.submit.bind(this)}
184
+ >${i18next.t('text.change password')}</md-elevated-button
185
+ >
107
186
  </form>
108
187
  `;
109
188
  }
@@ -130,8 +209,11 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
130
209
  if (params['new_pass'] !== params['confirm_pass']) {
131
210
  return this.showToast(i18next.t('error.new-password-and-confirm-password-do-not-match'));
132
211
  }
133
- auth.changePassword(params);
134
- this.form.reset();
212
+ /* Clear only once it took — a refused change used to wipe all three fields. */
213
+ if (await auth.changePassword(params)) {
214
+ this.form.reset();
215
+ this.changed = true;
216
+ }
135
217
  }
136
218
  showToast(message) {
137
219
  document.dispatchEvent(new CustomEvent('notify', { detail: { message } }));
@@ -143,8 +225,23 @@ __decorate([
143
225
  ], ChangePassword.prototype, "passwordRule", void 0);
144
226
  __decorate([
145
227
  query('form'),
146
- __metadata("design:type", HTMLFormElement)
228
+ __metadata("design:type", HTMLFormElement
229
+ /**
230
+ * Set once the change went through.
231
+ *
232
+ * The server answers with a message and the shell shows it as a toast, but a toast leaves and
233
+ * the same three empty boxes stay behind. A person who has just changed their password reads
234
+ * that as "nothing happened" — the report that brought this in said exactly that.
235
+ *
236
+ * So the form is replaced by a statement that stays. It also stops the boxes from inviting a
237
+ * second attempt at something already done.
238
+ */
239
+ )
147
240
  ], ChangePassword.prototype, "form", void 0);
241
+ __decorate([
242
+ state(),
243
+ __metadata("design:type", Object)
244
+ ], ChangePassword.prototype, "changed", void 0);
148
245
  ChangePassword = __decorate([
149
246
  customElement('change-password')
150
247
  ], ChangePassword);
@@ -1 +1 @@
1
- {"version":3,"file":"change-password.js","sourceRoot":"","sources":["../../client/components/change-password.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { FormFieldStyles } from '@operato/styles'\nimport { auth } from '@things-factory/auth-base/dist-client/auth.js'\nimport { generatePasswordPatternHelp, generatePasswordPatternRegExp } from '../utils/password-rule.js'\n\n@customElement('change-password')\nexport class ChangePassword extends localize(i18next)(LitElement) {\n static styles = [\n FormFieldStyles,\n css`\n * {\n box-sizing: border-box;\n }\n\n *:focus {\n outline: none;\n }\n\n form {\n display: flex;\n flex-direction: column;\n }\n\n /*\n * 색·테두리·모서리·포커스·placeholder 색은 FormFieldStyles 가 담당한다.\n * 여기서는 이 화면의 치수만 둔다 — 같은 프로필 화면의 위쪽 인풋(profile-component)과\n * 같은 값을 써서 한 화면에 두 가지 인상이 생기지 않게 한다.\n */\n input {\n border-radius: var(--change-password-field-border-radius, var(--border-radius));\n margin: var(--change-password-field-margin, var(--spacing-small) 0);\n padding: var(--change-password-field-padding, 9px);\n font: var(--change-password-field-font, var(--auth-input-field-font));\n width: var(--change-password-field-width, var(--auth-input-field-width));\n }\n\n ::placeholder {\n /*\n * 크기를 줄이지 않는다. 이전에는 0.8rem 이라 위쪽 인풋의 값(16px)보다 작게 보여\n * 같은 화면에서 패스워드 칸만 다른 스타일처럼 느껴졌다.\n */\n text-transform: capitalize;\n }\n\n md-elevated-button {\n margin: var(--spacing-small) auto var(--spacing-medium) auto;\n text-transform: capitalize;\n }\n\n button {\n /* fallback 도 테마 토큰으로 — 하드코딩 색은 다크에서 어긋난다 */\n background-color: var(--secondary-color, var(--md-sys-color-secondary));\n margin: 2px 2px 10px 2px;\n height: var(--button-height, 28px);\n color: var(--button-color, var(--md-sys-color-on-secondary));\n font: var(--button-font);\n border-radius: var(--button-radius, var(--md-sys-shape-corner-small, 5px));\n border: var(--button-border, 1px solid transparent);\n line-height: 1.5;\n }\n\n button:hover,\n button:active {\n background-color: var(--button-active-background-color, var(--md-sys-color-primary));\n border: var(--button-active-border);\n }\n\n /* 비밀번호 규칙 안내문 — 본문보다 낮은 강조. 라이트/다크 모두 읽히도록 토큰 파생. */\n .helper-text {\n font-size: 12px;\n color: var(--md-sys-color-on-surface-variant);\n margin-top: 4px;\n display: block; /* 텍스트를 입력 필드 아래에 배치 */\n line-height: 1.5; /* 텍스트 줄 간격 조절 */\n }\n `\n ]\n\n @property({ type: Object }) passwordRule: {\n lowerCase?: boolean\n upperCase?: boolean\n digit?: boolean\n specialCharacter?: boolean\n allowRepeat?: boolean\n useTightPattern?: boolean\n useLoosePattern?: boolean\n tightCharacterLength?: number\n looseCharacterLength?: number\n } = {}\n\n @query('form') form!: HTMLFormElement\n\n private passwordPattern: string = ''\n private passwordHelp: string = ''\n\n render() {\n return html`\n <form>\n <div class=\"field\">\n <input type=\"password\" name=\"current_pass\" placeholder=${i18next.t('text.current password')} required />\n </div>\n <span id=\"password-helper\" class=\"helper-text\">${this.passwordHelp}</span>\n <div class=\"field\">\n <input\n type=\"password\"\n name=\"new_pass\"\n placeholder=${i18next.t('text.new password')}\n required\n pattern=${this.passwordPattern}\n aria-describedby=\"password-helper\"\n />\n </div>\n <div class=\"field\">\n <input type=\"password\" name=\"confirm_pass\" placeholder=${i18next.t('text.confirm password')} required />\n </div>\n\n <md-elevated-button @click=${this.submit.bind(this)}>${i18next.t('text.change password')}</md-elevated-button>\n </form>\n `\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('passwordRule')) {\n this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)\n }\n }\n\n languageUpdated() {\n this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)\n }\n\n async submit() {\n const formData = new FormData(this.form)\n let params = {}\n for (const [key, value] of formData.entries()) {\n if (!value) {\n const placeholder = (this.form.querySelector(`[name=${key}]`) as HTMLInputElement)?.placeholder\n return this.showToast(i18next.t('error.value is empty', { value: placeholder || key }))\n }\n params[key] = value\n }\n\n if (params['new_pass'] !== params['confirm_pass']) {\n return this.showToast(i18next.t('error.new-password-and-confirm-password-do-not-match'))\n }\n\n auth.changePassword(params)\n this.form.reset()\n }\n\n showToast(message) {\n document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))\n }\n}\n"]}
1
+ {"version":3,"file":"change-password.js","sourceRoot":"","sources":["../../client/components/change-password.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { FormFieldStyles } from '@operato/styles'\nimport { auth } from '@things-factory/auth-base/dist-client/auth.js'\nimport { generatePasswordPatternHelp, generatePasswordPatternRegExp } from '../utils/password-rule.js'\n\n@customElement('change-password')\nexport class ChangePassword extends localize(i18next)(LitElement) {\n static styles = [\n FormFieldStyles,\n css`\n * {\n box-sizing: border-box;\n }\n\n *:focus {\n outline: none;\n }\n\n form {\n display: flex;\n flex-direction: column;\n }\n\n /*\n * 색·테두리·모서리·포커스·placeholder 색은 FormFieldStyles 가 담당한다.\n * 여기서는 이 화면의 치수만 둔다 — 같은 프로필 화면의 위쪽 인풋(profile-component)과\n * 같은 값을 써서 한 화면에 두 가지 인상이 생기지 않게 한다.\n */\n input {\n border-radius: var(--change-password-field-border-radius, var(--border-radius));\n margin: var(--change-password-field-margin, var(--spacing-small) 0);\n padding: var(--change-password-field-padding, 9px);\n font: var(--change-password-field-font, var(--auth-input-field-font));\n width: var(--change-password-field-width, var(--auth-input-field-width));\n }\n\n ::placeholder {\n /*\n * 크기를 줄이지 않는다. 이전에는 0.8rem 이라 위쪽 인풋의 값(16px)보다 작게 보여\n * 같은 화면에서 패스워드 칸만 다른 스타일처럼 느껴졌다.\n */\n text-transform: capitalize;\n }\n\n md-elevated-button {\n margin: var(--spacing-small) auto var(--spacing-medium) auto;\n text-transform: capitalize;\n }\n\n button {\n /* fallback 도 테마 토큰으로 — 하드코딩 색은 다크에서 어긋난다 */\n background-color: var(--secondary-color, var(--md-sys-color-secondary));\n margin: 2px 2px 10px 2px;\n height: var(--button-height, 28px);\n color: var(--button-color, var(--md-sys-color-on-secondary));\n font: var(--button-font);\n border-radius: var(--button-radius, var(--md-sys-shape-corner-small, 5px));\n border: var(--button-border, 1px solid transparent);\n line-height: 1.5;\n }\n\n button:hover,\n button:active {\n background-color: var(--button-active-background-color, var(--md-sys-color-primary));\n border: var(--button-active-border);\n }\n\n /* 비밀번호 규칙 안내문 — 본문보다 낮은 강조. 라이트/다크 모두 읽히도록 토큰 파생. */\n /* The statement that replaces the form once the change took. */\n .changed {\n display: flex;\n align-items: center;\n gap: 8px;\n /*\n * 같은 여백을 위쪽 인풋과 나눠 쓴다. 폼 상태에서는 첫 인풋의 margin 이 「비밀번호」\n * 라벨과의 간격을 만드는데, 이 문장은 margin 이 없어 라벨에 붙어 있었다 — 같은 자리에\n * 두 가지 간격이 생겼다. 값을 새로 정하지 않고 인풋과 같은 토큰을 쓴다.\n */\n margin: var(--change-password-field-margin, var(--spacing-small) 0);\n padding: 12px 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-left: 3px solid var(--md-sys-color-primary);\n border-radius: 7px;\n background: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n\n .changed md-icon {\n color: var(--md-sys-color-primary);\n }\n\n .changed button {\n margin-left: auto;\n border: 1px solid var(--md-sys-color-outline-variant);\n background: transparent;\n color: inherit;\n border-radius: 7px;\n padding: 5px 11px;\n cursor: pointer;\n }\n\n .helper-text {\n font-size: 12px;\n color: var(--md-sys-color-on-surface-variant);\n margin-top: 4px;\n display: block; /* 텍스트를 입력 필드 아래에 배치 */\n line-height: 1.5; /* 텍스트 줄 간격 조절 */\n }\n `\n ]\n\n @property({ type: Object }) passwordRule: {\n lowerCase?: boolean\n upperCase?: boolean\n digit?: boolean\n specialCharacter?: boolean\n allowRepeat?: boolean\n useTightPattern?: boolean\n useLoosePattern?: boolean\n tightCharacterLength?: number\n looseCharacterLength?: number\n } = {}\n\n @query('form') form!: HTMLFormElement\n\n /**\n * Set once the change went through.\n *\n * The server answers with a message and the shell shows it as a toast, but a toast leaves and\n * the same three empty boxes stay behind. A person who has just changed their password reads\n * that as \"nothing happened\" — the report that brought this in said exactly that.\n *\n * So the form is replaced by a statement that stays. It also stops the boxes from inviting a\n * second attempt at something already done.\n */\n @state() private changed = false\n\n private passwordPattern: string = ''\n private passwordHelp: string = ''\n\n render() {\n if (this.changed) {\n return html`\n <div class=\"changed\" role=\"status\">\n <md-icon>check_circle</md-icon>\n <span>${i18next.t('text.password changed successfully')}</span>\n <button type=\"button\" @click=${() => (this.changed = false)}>\n ${i18next.t('text.change password')}\n </button>\n </div>\n `\n }\n\n return html`\n <form>\n <div class=\"field\">\n <input type=\"password\" name=\"current_pass\" placeholder=${i18next.t('text.current password')} required />\n </div>\n <!--\n The rule belongs under the field the rule applies to.\n \n It used to sit between the two, directly beneath the current password, so a sighted\n reader took it as the rule for that field — and the current password has no reason to\n satisfy today's rule. It may predate it. The server only checks that it matches\n (change-pwd.ts verifies the current one and validates the new one against the rule).\n aria-describedby always pointed at the new field, so a screen reader had it right and\n only the eye was misled.\n -->\n <div class=\"field\">\n <input\n type=\"password\"\n name=\"new_pass\"\n placeholder=${i18next.t('text.new password')}\n required\n pattern=${this.passwordPattern}\n aria-describedby=\"password-helper\"\n />\n </div>\n <span id=\"password-helper\" class=\"helper-text\">${this.passwordHelp}</span>\n <div class=\"field\">\n <input type=\"password\" name=\"confirm_pass\" placeholder=${i18next.t('text.confirm password')} required />\n </div>\n\n <!--\n type=\"button\", the way abstract-sign and abstract-auth-page already declare it.\n \n A Material button inside a form defaults to type=\"submit\" and submits unless the click\n is default-prevented. So one click ran submit() and then let the browser submit the\n form as well. submit() had already called form.reset(), so constraint validation found\n three empty required fields, rejected the first one, and put the caret back on the\n current password with an English \"Please fill out this field.\" bubble on a Korean\n screen. It read as the current password being refused.\n [확인함] 2026-09-09 · 사용자 보고 · 화면 사진\n -->\n <md-elevated-button type=\"button\" @click=${this.submit.bind(this)}\n >${i18next.t('text.change password')}</md-elevated-button\n >\n </form>\n `\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('passwordRule')) {\n this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)\n }\n }\n\n languageUpdated() {\n this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)\n }\n\n async submit() {\n const formData = new FormData(this.form)\n let params = {}\n for (const [key, value] of formData.entries()) {\n if (!value) {\n const placeholder = (this.form.querySelector(`[name=${key}]`) as HTMLInputElement)?.placeholder\n return this.showToast(i18next.t('error.value is empty', { value: placeholder || key }))\n }\n params[key] = value\n }\n\n if (params['new_pass'] !== params['confirm_pass']) {\n return this.showToast(i18next.t('error.new-password-and-confirm-password-do-not-match'))\n }\n\n /* Clear only once it took — a refused change used to wipe all three fields. */\n if (await auth.changePassword(params)) {\n this.form.reset()\n this.changed = true\n }\n }\n\n showToast(message) {\n document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))\n }\n}\n"]}
@@ -0,0 +1,144 @@
1
+ import '@material/web/icon/icon.js';
2
+ import '@material/web/button/elevated-button.js';
3
+ import '@material/web/button/outlined-button.js';
4
+ import '@material/web/textfield/filled-text-field.js';
5
+ import '@material/web/checkbox/checkbox.js';
6
+ import { LitElement } from 'lit';
7
+ import type { CSSResultGroup } from 'lit';
8
+ declare const RoleDetail_base: (new (...args: any[]) => LitElement) & typeof LitElement;
9
+ /**
10
+ * 한 역할이 무엇을 열 수 있나 — **모듈 × 읽기·쓰기** 격자로 고친다.
11
+ *
12
+ * ── 옛 화면이 무엇을 잘못했나 (`components/role-privilege-editor.ts`) ────────
13
+ * 세 개가 함께 있었고 셋 다 조용했다.
14
+ *
15
+ * **① A 의 이름이 B 에 저장된다.** 이름 입력이 `roleChanged` 를 매 글자마다 페이지로 올리고,
16
+ * 페이지가 그것을 `updateRoleObj` 한 칸에 담아 **모든** 권한 편집기에 내려 줬다. 저장은
17
+ * `{ ...updateRoleObj, privileges }` 였다. A 를 열어 이름을 고치고 B 를 열어 저장하면 B 의
18
+ * 이름이 A 가 된다. 여기서는 이름·설명이 **이 컴포넌트의 상태**다 — 페이지가 들지 않는다.
19
+ *
20
+ * **② 이름으로 남을 찾는다.** 권한을 읽을 때 `role(name:)` 를 썼는데, 그 질의의 범위는
21
+ * 「내 도메인 + 부모」다. 부모에 같은 이름의 역할이 있으면 **다른 역할의 권한을 그렸고**,
22
+ * 저장은 손에 든 id 로 갔다. 여기서는 `rolePrivileges(roleId:)` 하나로 읽는다.
23
+ *
24
+ * **③ 저장 범위를 DOM 에서 만든다.** `querySelectorAll('input:checked')` 로 보냈다. 접히거나
25
+ * 안 그려진 것은 「끈 것」이 되어 **조용히 떨어졌다.** 여기서는 서버가 준 전체 목록이 범위이고,
26
+ * 검색으로 걸러도 안 보이는 칸은 그대로 유지된다.
27
+ *
28
+ * ── 왜 격자인가 ─────────────────────────────────────────────────────────────
29
+ * 옛 화면은 84줄을 한 줄씩 늘어놓고 `description` 을 라벨로 썼다. 「board 를 편집할 수 있나」를
30
+ * 알려면 그 줄을 눈으로 찾아야 했다. 모듈이 행이고 읽기·쓰기가 열이면 **한 모듈에서 무엇이
31
+ * 열려 있나가 한 줄로 보인다.**
32
+ *
33
+ * 열은 **데이터가 정한다.** `execute` 는 아직 서버에 없으므로 열이 나오지 않고, 선언이 서면
34
+ * 화면을 고치지 않고 열이 생긴다 — 없는 것을 있는 것처럼 그리지 않는다.
35
+ *
36
+ * ── 빈 칸과 유령 ────────────────────────────────────────────────────────────
37
+ * 칸이 아예 없는 것(`—`)은 「끈 것」이 아니라 **그 모듈에 그런 권한이 선언되지 않았다**는 뜻이다.
38
+ * 유령(이 배포의 어떤 코드도 요구하지 않는 권한)은 취소선으로 보이고, **켜는 것만 막는다** —
39
+ * 이미 켜져 있는 것은 떼야 하니까.
40
+ */
41
+ export declare class RoleDetail extends RoleDetail_base {
42
+ static styles: CSSResultGroup;
43
+ /** 목록이 골라 준 역할. `id` 가 있어야 한다. 없으면 새로 만드는 자리다. */
44
+ role: any;
45
+ private name;
46
+ private description;
47
+ /** 서버가 준 전체 권한. **이것이 저장 범위다** — 화면이 무엇을 그렸는지와 무관하다. */
48
+ private rows;
49
+ private axes;
50
+ private assigned;
51
+ private holders;
52
+ private mySubdomain;
53
+ /** 지우기만 선언이 다르다 — 소유권 우회가 없다. 그 선언을 그대로 되묻는다. */
54
+ private mayDelete;
55
+ private keyword;
56
+ private loading;
57
+ private failed;
58
+ private working;
59
+ private message;
60
+ private get creating();
61
+ /**
62
+ * **부모 도메인의 역할은 여기서 못 고친다.**
63
+ *
64
+ * 목록은 상속으로 부모 역할까지 보여 준다 — 실제로 부여할 수 있는 범위가 그것이기 때문이다.
65
+ * 그런데 `updateRole`·`deleteRole` 은 `domain.id` 하나로 좁혀 찾으므로 부모 것은 못 찾는다.
66
+ * 그래서 여기서는 **읽기 전용으로 두고 어디로 가야 하는지 말한다** — 눌리는 단추를 두고 서버가
67
+ * 거절하면 그것은 안내가 아니라 오류다.
68
+ */
69
+ private get inherited();
70
+ /** 저장할 수 있나 — 단추와 처리기가 같은 답을 본다. */
71
+ private get savable();
72
+ /**
73
+ * 지울 수 있나.
74
+ *
75
+ * 서버가 **가진 사람이 있으면 거절한다**(`deleteRole` — 「cannot delete role because it is
76
+ * currently in use」). 그러니 사람 목록이 비어 있을 때만 내준다. 그리고 이 하나만 선언이 다르다 —
77
+ * `@privilege(category:"user", privilege:"mutation")` 로 **소유권 우회가 없다.** 만들기·고치기는
78
+ * 소유자에게 열려 있는데 지우기만 권한을 요구한다.
79
+ */
80
+ private get deletable();
81
+ /** 보여 줄 행. **저장 범위가 아니다.** */
82
+ private get visible();
83
+ render(): import("lit-html").TemplateResult<1>;
84
+ /**
85
+ * 역할 지우기.
86
+ *
87
+ * **왜 안 되는지를 말한다.** 서버는 가진 사람이 있으면 거절하는데(`deleteRole`), 옛 화면은
88
+ * 언제나 단추를 보여 주고 눌린 뒤에 401 을 받았다. 여기서는 사람이 있으면 단추 대신 그 사실을
89
+ * 적는다 — 관리자가 다음에 할 일(사람에게서 역할을 떼는 것)이 그 문장에서 나온다.
90
+ */
91
+ private renderDangerZone;
92
+ private renderPrivileges;
93
+ /**
94
+ * 한 칸.
95
+ *
96
+ * ⚠ **설명이 없다.** `description` 은 권한 행의 칸인데 **아무도 채우지 않는다** — 부팅 동기화가
97
+ * `{ category, name }` 둘만 저장하고(`auth-base/server/index.ts:34-36`), `createPrivilege` 는
98
+ * 그 칸을 받지만 그것을 부르는 화면이 없다. 그래서 713개 선언이 전부 빈 설명이다.
99
+ *
100
+ * 없을 때 `${'$'}{category} ${'$'}{axis}` 를 보여 주던 것을 걷어냈다 — 그건 **코드 낱말을 한 번 더**
101
+ * 보여 주는 것이고, 관리자가 「배울 것이 없다」고 한 자리가 정확히 그것이다. 지금은 없다고
102
+ * 말한다. 지어내지 않는다.
103
+ *
104
+ * 글이 어디서 오는지는 UX 레인이 정하고 있다(`privilege.category.*` 키가 en·ko 에 이미 하나
105
+ * 있고 나머지 45개가 비어 있다). 그 키가 오면 이 자리는 tooltip 이 아니라 **본문**으로 옮긴다.
106
+ */
107
+ private renderCell;
108
+ /**
109
+ * 이 역할을 가진 사람.
110
+ *
111
+ * 「권한을 어디서 찾나」의 반대 방향이다 — 권한을 고르기 전에 **이걸 바꾸면 누가 영향을
112
+ * 받는지** 보이는 것이 사람 목록이다. 새로 만드는 자리에는 아직 없다.
113
+ */
114
+ private renderHolders;
115
+ private cellCount;
116
+ connectedCallback(): void;
117
+ /** 서버 선언을 그대로 되묻고, 지금 도메인을 프로필에서 읽는다. */
118
+ private checkGates;
119
+ updated(changes: Map<string, unknown>): void;
120
+ /**
121
+ * 한 번의 질의로 **전체 목록과 이 역할이 가진 것**을 함께 읽는다(`rolePrivileges(roleId:)`).
122
+ *
123
+ * 새로 만드는 자리는 역할이 없으니 `privileges` 로 목록만 읽는다 — 그때 켜진 것은 없다.
124
+ */
125
+ private fetch;
126
+ /** 평평한 목록을 격자로 세운다. 열은 데이터에 있는 축만. */
127
+ private build;
128
+ private onToggle;
129
+ /**
130
+ * 저장.
131
+ *
132
+ * **범위는 `rows` 다.** 검색으로 걸러 놓고 저장해도 안 보이던 칸은 켜진 채로 간다 — 옛 화면이
133
+ * DOM 을 훑어 조용히 떼던 자리다.
134
+ */
135
+ private onSave;
136
+ /**
137
+ * 역할을 지운다.
138
+ *
139
+ * **누가 잃는지 먼저 말한다.** 옛 화면은 「역할을 삭제하시겠습니까」만 물었다 — 그 역할을 가진
140
+ * 사람이 몇인지는 같은 화면에 있었는데도 확인 문구에 없었다.
141
+ */
142
+ private onDelete;
143
+ }
144
+ export {};