@things-factory/auth-ui 10.1.6 → 10.1.13

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 (45) hide show
  1. package/dist-client/components/role-privilege-picker-state.d.ts +11 -0
  2. package/dist-client/components/role-privilege-picker-state.js +15 -0
  3. package/dist-client/components/role-privilege-picker-state.js.map +1 -0
  4. package/dist-client/components/role-privilege-picker-state.test.js +20 -0
  5. package/dist-client/components/role-privilege-picker-state.test.js.map +1 -0
  6. package/dist-client/components/role-privilege-picker.d.ts +40 -0
  7. package/dist-client/components/role-privilege-picker.js +77 -0
  8. package/dist-client/components/role-privilege-picker.js.map +1 -0
  9. package/dist-client/components/role-template-picker.d.ts +83 -0
  10. package/dist-client/components/role-template-picker.js +487 -0
  11. package/dist-client/components/role-template-picker.js.map +1 -0
  12. package/dist-client/pages/role/role-list-page.d.ts +104 -13
  13. package/dist-client/pages/role/role-list-page.js +396 -72
  14. package/dist-client/pages/role/role-list-page.js.map +1 -1
  15. package/dist-client/pages/role/role-privilege-page.d.ts +113 -37
  16. package/dist-client/pages/role/role-privilege-page.js +426 -139
  17. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  18. package/dist-client/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +4 -4
  20. package/translations/en.json +27 -1
  21. package/translations/ja.json +14 -1
  22. package/translations/ko.json +26 -0
  23. package/translations/ms.json +15 -2
  24. package/translations/zh.json +14 -1
  25. package/dist-client/components/create-role.d.ts +0 -1
  26. package/dist-client/components/create-role.js +0 -117
  27. package/dist-client/components/create-role.js.map +0 -1
  28. package/dist-client/components/credential-manager.d.ts +0 -11
  29. package/dist-client/components/credential-manager.js +0 -64
  30. package/dist-client/components/credential-manager.js.map +0 -1
  31. package/dist-client/components/role-detail.d.ts +0 -144
  32. package/dist-client/components/role-detail.js +0 -698
  33. package/dist-client/components/role-detail.js.map +0 -1
  34. package/dist-client/components/role-edit-form.js +0 -95
  35. package/dist-client/components/role-edit-form.js.map +0 -1
  36. package/dist-client/components/role-privilege-editor.d.ts +0 -1
  37. package/dist-client/components/role-privilege-editor.js +0 -368
  38. package/dist-client/components/role-privilege-editor.js.map +0 -1
  39. package/dist-client/pages/role/role-management.d.ts +0 -4
  40. package/dist-client/pages/role/role-management.js +0 -137
  41. package/dist-client/pages/role/role-management.js.map +0 -1
  42. package/dist-client/pages/user/user-management.d.ts +0 -35
  43. package/dist-client/pages/user/user-management.js +0 -223
  44. package/dist-client/pages/user/user-management.js.map +0 -1
  45. /package/dist-client/components/{role-edit-form.d.ts → role-privilege-picker-state.test.d.ts} +0 -0
@@ -6,6 +6,7 @@ import gql from 'graphql-tag';
6
6
  import { css, html, nothing } from 'lit';
7
7
  import { customElement, property, query, state } from 'lit/decorators.js';
8
8
  import { client, gqlContext } from '@operato/graphql';
9
+ import { openPopup } from '@operato/layout';
9
10
  import { OxPrompt } from '@operato/popup/ox-prompt.js';
10
11
  import { i18next, localize } from '@operato/i18n';
11
12
  import { p13n } from '@operato/p13n';
@@ -14,25 +15,30 @@ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles } from '@operato
14
15
  import { isMobileDevice } from '@operato/utils';
15
16
  import { getDomain } from '@things-factory/auth-base/dist-client';
16
17
  /**
17
- * 역할 목록 — **무엇을 여는 역할인지 목록에서 읽힌다.**
18
+ * 역할 목록 — **보이는 것과 고칠 있는 것을 가른다.**
18
19
  *
19
20
  * ── 옛 화면이 하던 것 (`pages/role/role-management.ts`) ──────────────────────
20
21
  * `quick-find-list` 안에 역할을 늘어놓고, 펼치면 그 안에 이름 입력칸과 84줄의 권한 체크박스가
21
- * 함께 있었다. 그 구조에서 가지가 났다.
22
+ * 함께 있었다. 그 구조에서 셋이 났다.
22
23
  *
23
24
  * ① 이름 입력이 매 글자마다 페이지로 올라가 **모든** 편집기에 공유됐다 — A 를 고치고 B 를
24
- * 저장하면 B 의 이름이 A 가 됐다. 지금은 `role-detail` 자기 이름을 들고 있다.
25
+ * 저장하면 B 의 이름이 A 가 됐다. 지금은 이름·설명을 격자에서 고친다.
25
26
  * ② 목록에 권한이 하나도 안 보였다. 「이 역할이 무엇을 여나」를 알려면 펼쳐야 했다.
26
27
  * ③ 한도도 쪽 나누기도 없었다.
27
28
  *
28
- * ── 목록이 말해 주는 ─────────────────────────────────────────────────────
29
- * 역할마다 **몇 개의 모듈에서 무엇이 열려 있나**를 요약해 싣는다(`board 읽기·쓰기 · order 읽기`
30
- * 식). 사용자 관리에서 역할을 고를 이름만 보고 고르던 것이 여기서 갈린다.
29
+ * ── 상속이 화면의 중심이다 ───────────────────────────────────────────────
30
+ * 목록은 부모 도메인의 역할까지 보여 준다 **부여할 있는 범위가 그것**이기 때문이다.
31
+ * 그런데 행은 여기서 고친다(`updateRole` `domain.id` 하나로 좁혀 찾는다).
31
32
  *
32
- * ── 상속 ────────────────────────────────────────────────────────────────────
33
- * 부모 도메인의 역할도 함께 나온다(`inherited: Include`) 실제로 부여할 있는 범위가 그것이기
34
- * 때문이다. 다만 **부모 것은 여기서 고친다**(`updateRole` 범위가 도메인이다). 그래서
35
- * 어느 도메인의 역할인지 컬럼으로 말한다.
33
+ * 전에는 `Include` 로 열어서 **못 고치는 9행이 고칠 수 있는 2행 앞에 깔렸다**(UX 실측).
34
+ * 지금은 「우리 역할」로 열고, 묶음의 수를 함께 적어 **상속이 있다는 사실 자체는 남긴다.**
35
+ * 편집도 `editable` 함수로 상속 행에서 닫고, 삭제도 상속 행을 보낸다.
36
+ *
37
+ * ── 권한은 수만 낸다 ────────────────────────────────────────────────────────
38
+ * 전에는 `activity 쓰기 · activity-instance 쓰기·읽기` 식으로 잘린 코드 용어를 그렸고, 그것을
39
+ * 그리려고 권한 **행**을 받았다(UX 실측 5,731 bytes · 69ms 대 518 bytes · 7ms). 지금은
40
+ * `privilegeCount` 하나를 받는다. 권한 편집으로 가는 문은 **버튼 gutter 칸**이 진다 —
41
+ * 눌리는 숫자는 손잡이로 보이지 않는다.
36
42
  */
37
43
  const RoleListPageBase = p13n(localize(i18next)(PageView));
38
44
  let RoleListPage = class RoleListPage extends RoleListPageBase {
@@ -54,6 +60,25 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
54
60
  * 견준다 — 두 도메인이 같은 subdomain 을 가질 수 없으므로 id 와 같은 답이다.
55
61
  */
56
62
  this.mySubdomain = '';
63
+ /*
64
+ * **어느 묶음을 보고 있나 — 기본값은 「우리 역할」이다.**
65
+ *
66
+ * 전에는 `Include` 로 열었다. 그러면 처음 화면에 **못 고치는 행 9개가 앞에 깔린다**
67
+ * (UX 실측: miratek 2 · system 9). `None` 으로 열면 처음 화면이 2행이고 **그 2행이 전부
68
+ * 고칠 수 있는 행**이다.
69
+ *
70
+ * 세 묶음은 서버에 이미 있다(`InheritedValueType`). 새로 만들 것이 없다.
71
+ */
72
+ this.scope = InheritedValueType.None;
73
+ /**
74
+ * 묶음마다 몇 건인가.
75
+ *
76
+ * 수를 함께 적는 이유는 **상속이 있다는 사실 자체가 보이게** 하는 것이다. 기본값을 「우리
77
+ * 역할」로 좁혀 놓으면, 수가 없으면 상속 9건이 존재하는지조차 모른다.
78
+ *
79
+ * `undefined` 는 「아직 못 셌다」다 — 0 으로 메우면 「없다」와 같아 보인다.
80
+ */
81
+ this.counts = {};
57
82
  }
58
83
  static { this.styles = [
59
84
  ScrollbarStyles,
@@ -88,6 +113,84 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
88
113
  background-color: var(--md-sys-color-error-container);
89
114
  color: var(--md-sys-color-on-error-container);
90
115
  }
116
+ /* 「우리」는 기본 상태다 — 색을 쓰면 색이 아무것도 말하지 않게 된다. */
117
+ .badge[quiet] {
118
+ background-color: transparent;
119
+ color: var(--md-sys-color-outline);
120
+ }
121
+
122
+ /*
123
+ ── 세 묶음 ────────────────────────────────────────────────────────────
124
+ 고른 것은 면으로 말한다. 글자색만 바꾸면 야간에 안 보인다.
125
+ */
126
+ [scopes] {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 4px;
130
+ }
131
+
132
+ button[scope] {
133
+ display: inline-flex;
134
+ align-items: baseline;
135
+ gap: 5px;
136
+ padding: 5px 12px;
137
+ font: var(--label-font, inherit);
138
+ font-size: 0.82rem;
139
+ color: var(--md-sys-color-on-surface-variant);
140
+ background-color: transparent;
141
+ border: 1px solid var(--md-sys-color-outline-variant);
142
+ border-radius: var(--md-sys-shape-corner-full, 999px);
143
+ cursor: pointer;
144
+ white-space: nowrap;
145
+ }
146
+
147
+ button[scope]:hover {
148
+ background-color: var(--md-sys-color-surface-variant);
149
+ }
150
+
151
+ button[scope][active] {
152
+ color: var(--md-sys-color-on-secondary-container);
153
+ background-color: var(--md-sys-color-secondary-container);
154
+ border-color: var(--md-sys-color-secondary-container);
155
+ }
156
+
157
+ button[scope] [count] {
158
+ font-size: 0.78rem;
159
+ color: var(--md-sys-color-outline);
160
+ }
161
+
162
+ button[scope][active] [count] {
163
+ color: var(--md-sys-color-on-secondary-container);
164
+ }
165
+
166
+ /*
167
+ * 새 역할 단추 — **옆에 선 것들을 따라간다.**
168
+ *
169
+ * 이 줄에는 알약 범위 단추 셋과 검색이 서 있다. 그 사이에서 이것만 네모난 기본 단추면
170
+ * 같은 줄의 문법이 깨진다. 범위 단추와 같은 모서리·같은 테두리를 쓰되, **여는 일**이므로
171
+ * 채워서 한 단 앞으로 낸다 — 나머지는 거르는 일이고 이것만 만드는 일이다.
172
+ */
173
+ button[add] {
174
+ display: inline-flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ padding: 5px 10px;
178
+ color: var(--md-sys-color-on-secondary-container);
179
+ background-color: var(--md-sys-color-secondary-container);
180
+ border: 1px solid transparent;
181
+ border-radius: var(--md-sys-shape-corner-full, 999px);
182
+ cursor: pointer;
183
+ margin-left: auto;
184
+ }
185
+
186
+ button[add]:hover {
187
+ background-color: var(--md-sys-color-secondary);
188
+ color: var(--md-sys-color-on-secondary);
189
+ }
190
+
191
+ button[add] md-icon {
192
+ --md-icon-size: 18px;
193
+ }
91
194
 
92
195
  ox-oops-note[note] {
93
196
  position: absolute;
@@ -103,10 +206,31 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
103
206
  * 격자에서 고치는 화면들의 관례다(`fiscal-quarter-list-page` 외). 격자 안 편집은 여러 줄을
104
207
  * 한꺼번에 고치므로, 줄마다 저장 단추를 두면 어디를 눌러야 하는지가 사라진다.
105
208
  */
209
+ /**
210
+ * 찾기는 **제목 줄**에 선다 — 셸이 그 자리를 모든 화면에 같은 모양으로 둔다.
211
+ *
212
+ * 이 페이지는 한동안 `ox-filters-form` 을 헤드룸의 칩 줄에 **직접** 끼워 넣고 있었다. 그래서
213
+ * 돋보기가 범위 칩과 만들기 단추 사이에 끼어, 같은 앱의 다른 목록과 찾기 자리가 달랐다
214
+ * (`env-var-list-page` 외 auth-ui 목록 다섯이 전부 아래 모양이다).
215
+ *
216
+ * 둘은 대안이 아니라 **다른 것**이다 — `search` 는 목록 전체를 훑는 글자 찾기이고,
217
+ * `filter` 는 칸별 조건 폼을 접었다 펴는 것이다. 표준 화면은 둘 다 선언한다.
218
+ */
106
219
  get context() {
107
220
  return {
108
221
  title: i18next.t('text.role_management'),
109
222
  help: 'auth/roles',
223
+ search: {
224
+ handler: (search) => {
225
+ this.grist.searchText = search;
226
+ },
227
+ value: this.grist?.searchText || ''
228
+ },
229
+ filter: {
230
+ handler: () => {
231
+ this.grist.toggleHeadroom();
232
+ }
233
+ },
110
234
  actions: [
111
235
  { title: i18next.t('button.save'), action: this.onSaveGrid.bind(this) },
112
236
  { title: i18next.t('button.delete'), action: this.onDeleteGrid.bind(this) }
@@ -123,12 +247,18 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
123
247
  >
124
248
  <div slot="headroom" class="header">
125
249
  <div class="filters">
126
- <ox-filters-form autofocus></ox-filters-form>
250
+ ${this.renderScopes()}
127
251
 
128
- <!-- 목록에서 줄을 만드는 관례 (24 파일). 격자 설정에서 칸을 읽어 온다. -->
129
- <ox-record-creator .callback=${this.onCreateRole.bind(this)}>
130
- <button><md-icon>add</md-icon></button>
131
- </ox-record-creator>
252
+ <!-- 찾기는 제목 줄이 갖는다(context.search). 폼이 자기 검색칸을 그리면 둘이 된다. -->
253
+ <ox-filters-form without-search></ox-filters-form>
254
+
255
+ <!--
256
+ 여기 ox-record-creator 가 있었다 — 목록에 빈 줄을 만들고 이름을 치게 하는
257
+ 관례(24 파일)다. 걷어낸 이유는 openCreate 의 머리말에 있다.
258
+ -->
259
+ <button add @click=${() => this.openCreate()} title=${i18next.t('button.create')}>
260
+ <md-icon>add</md-icon>
261
+ </button>
132
262
  </div>
133
263
  <ox-context-page-toolbar class="actions" .context=${this.context}></ox-context-page-toolbar>
134
264
  </div>
@@ -139,6 +269,67 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
139
269
  ${this.renderNote()}
140
270
  `;
141
271
  }
272
+ /**
273
+ * 세 묶음.
274
+ *
275
+ * **수를 함께 적는 것이 이 부품의 이유다.** 기본값을 「우리 역할」로 좁혀 두면 처음 화면이
276
+ * 깨끗한 대신 **상속 9건이 있다는 사실이 사라진다.** 수가 그것을 되돌려 놓는다.
277
+ *
278
+ * 아직 못 센 묶음은 수를 **안 그린다** — 0 으로 메우면 「없다」와 같아 보인다.
279
+ */
280
+ renderScopes() {
281
+ const scopes = [
282
+ [InheritedValueType.None, 'label.ours'],
283
+ [InheritedValueType.Only, 'label.inherited'],
284
+ [InheritedValueType.Include, 'label.all']
285
+ ];
286
+ return html `
287
+ <div scopes>
288
+ ${scopes.map(([scope, key]) => html `
289
+ <button scope ?active=${this.scope === scope} @click=${() => this.onScope(scope)}>
290
+ ${i18next.t(key)}
291
+ ${this.counts[scope] === undefined ? nothing : html `<span count>${this.counts[scope]}</span>`}
292
+ </button>
293
+ `)}
294
+ </div>
295
+ `;
296
+ }
297
+ onScope(scope) {
298
+ if (this.scope === scope) {
299
+ return;
300
+ }
301
+ this.scope = scope;
302
+ this.grist.fetch();
303
+ }
304
+ /**
305
+ * 세 묶음의 건수.
306
+ *
307
+ * 세 번 묻는다. 한 번에 셋을 세는 질의가 서버에 없고, 각각 `total` 만 받으므로 가볍다
308
+ * (UX 실측 158·566·684 bytes). 실패하면 **수를 안 그린다** — 틀린 수를 그리는 것보다
309
+ * 없는 것이 낫다.
310
+ */
311
+ async countScopes() {
312
+ const scopes = [InheritedValueType.None, InheritedValueType.Only, InheritedValueType.Include];
313
+ const counted = await Promise.all(scopes.map(async (scope) => {
314
+ const response = await client.query({
315
+ query: gql `
316
+ query ($inherited: InheritedValueType) {
317
+ roles(inherited: $inherited, pagination: { page: 1, limit: 1 }) {
318
+ total
319
+ }
320
+ }
321
+ `,
322
+ variables: { inherited: scope }
323
+ });
324
+ return [scope, response.errors?.length ? undefined : response.data?.roles?.total];
325
+ }));
326
+ this.counts = counted.reduce((counts, [scope, total]) => {
327
+ if (total !== undefined) {
328
+ counts[scope] = total;
329
+ }
330
+ return counts;
331
+ }, {});
332
+ }
142
333
  /**
143
334
  * 목록 위에 겹쳐 그린다 — 격자를 떼면 다시 읽을 길이 없어진다.
144
335
  *
@@ -181,15 +372,85 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
181
372
  const subdomain = record?.domain?.subdomain;
182
373
  return Boolean(subdomain) && subdomain !== this.mySubdomain;
183
374
  }
375
+ /**
376
+ * 출처 한 칸.
377
+ *
378
+ * 「우리」는 조용히 둔다 — 기본 상태에 색을 쓰면 색이 아무것도 말하지 않게 된다.
379
+ * 상속은 **어느 도메인에서 왔는지 함께 적는다.** 「상속」만 적으면 관리자가 어디로 가야
380
+ * 하는지 모른다.
381
+ */
382
+ sourceCell(record) {
383
+ if (!this.isInherited(record)) {
384
+ return html `<span class="badge" quiet>${i18next.t('label.ours')}</span>`;
385
+ }
386
+ const from = record?.domain?.name || record?.domain?.subdomain || '';
387
+ return html `<span class="badge" title=${i18next.t('text.role belongs to x domain and is edited there', { x: from })}
388
+ >${i18next.t('label.inherited')}${from ? ` · ${from}` : ''}</span
389
+ >`;
390
+ }
184
391
  async pageInitialized() {
185
392
  this.mySubdomain = (await getDomain())?.subdomain || '';
393
+ this.countScopes();
186
394
  this.config = {
395
+ /*
396
+ * **두 모드가 다른 말을 하지 않게 한다.**
397
+ *
398
+ * ⚠ 여기가 `summary`·`domain` 을 가리킨 채로 남아 있었다 — 컬럼을 `privilegeCount`·
399
+ * `source` 로 갈았는데 이 설정을 안 고쳤다. 그래서 **LIST 모드에서는 권한 수도 출처도
400
+ * 아예 안 그려졌다.** 이름이 틀려도 격자가 조용히 넘어가므로 빌드도 시험도 못 잡는다.
401
+ *
402
+ * 그리고 이 앱이 실제로 LIST 로 연다(`isMobileDevice()` 또는 사람마다 기억된 개인 설정).
403
+ * GRID 만 보고 만들면 그 사람들은 내가 만든 칸을 한 번도 못 본다.
404
+ */
187
405
  list: {
188
406
  fields: ['name', 'description'],
189
- details: ['summary', 'domain']
407
+ details: ['privilegeCount', 'source']
190
408
  },
191
409
  columns: [
192
410
  { type: 'gutter', gutterName: 'sequence', fixed: true },
411
+ {
412
+ /*
413
+ * **권한 편집으로 가는 문.**
414
+ *
415
+ * `gutterName: 'button'` 이 이 저장소의 관례다 — 68 파일 · 104 자리
416
+ * (`employee-list-page.ts:110-124` 가 같은 모양). `header` 에 이름이 붙은 아이콘 칸은
417
+ * **손잡이로 보인다.** 그것이 「권한 편집 방법을 모르겠다」를 고치는 자리다.
418
+ *
419
+ * 행 전체에 클릭을 두지 않는 이유는 그대로다 — 이름을 고치려 누른 것이 화면을
420
+ * 떠나면 안 된다.
421
+ *
422
+ * ── 상속 행에서도 버튼은 남긴다 ─────────────────────────────────────
423
+ * **읽는 것은 정당하다.** 상속 역할이 무엇을 여는지 보는 것은 막을 이유가 없고,
424
+ * 오히려 「우리 역할로 복사」를 하려면 먼저 봐야 한다. `icon`·`title` 이 record 를
425
+ * 받으므로 그 구분이 여기 실린다 — 글자만 「보기만」으로 바뀐다.
426
+ */
427
+ type: 'gutter',
428
+ gutterName: 'button',
429
+ header: i18next.t('field.privileges'),
430
+ /*
431
+ * ⚠ **아직 저장 안 된 빈 줄에는 내주지 않는다.** 격자가 데이터 2행 아래에 추가용 빈
432
+ * 줄을 하나 더 그리는데, 거기에도 버튼이 나왔다. 눌러도 `record.id` 가 없어 아무 일이
433
+ * 없다 — **조용한 무동작 단추**다.
434
+ *
435
+ * `employee-list-page` 가 `icon` 에서 `''` 를 돌려주는 이유가 이것이었다. 모양만
436
+ * 베끼고 그 가드를 안 가져왔다.
437
+ */
438
+ icon: (record) => (record?.id ? 'key' : ''),
439
+ iconOnly: false,
440
+ title: (record) => !record?.id
441
+ ? ''
442
+ : this.isInherited(record)
443
+ ? i18next.t('text.view only')
444
+ : i18next.t('button.edit privileges'),
445
+ width: 92,
446
+ handlers: {
447
+ click: (_columns, _data, _column, record) => {
448
+ if (record?.id) {
449
+ navigate(`role-privileges/${record.id}`);
450
+ }
451
+ }
452
+ }
453
+ },
193
454
  {
194
455
  type: 'string',
195
456
  name: 'name',
@@ -218,34 +479,47 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
218
479
  },
219
480
  {
220
481
  /*
221
- * **이 역할이 무엇을 여나.** 화면은 이것을 보려면 펼쳐야 했다.
222
- * 권한이 하나도 없는 역할은 붙여도 아무 일이 없으므로 눈에 걸리게 그린다.
482
+ * **이 역할이 여는 것이 개인가.** 읽는 값이고, 누르는 자리가 아니다.
483
+ *
484
+ * 전에는 여기에 `activity 쓰기 · activity-instance 쓰기·읽기 · 3개 더` 식으로 잘린
485
+ * 코드 용어를 그렸다. 그것을 그리려고 목록이 권한 **행**을 받았다.
486
+ *
487
+ * ```
488
+ * [UX 실측] roles(limit:20) 권한 포함 5,731 bytes · 69ms
489
+ * [UX 실측] 권한 없이 518 bytes · 7ms → 11.1배 · 10배
490
+ * ```
491
+ *
492
+ * 수는 잘린 문자열이 못 하던 것을 한다 — 「더 있다」가 아니라 **얼마인지** 말하고,
493
+ * `0` 이면 아직 아무것도 안 열린 역할이라는 것이 한눈에 보인다.
494
+ *
495
+ * ⚠ **여기에 클릭을 얹지 않는다.** 한때 그렇게 뒀는데, 읽는 값과 누르는 자리가 한 칸에
496
+ * 들어간다. 그리고 사용자 평가의 첫 줄이 「권한 편집 방법을 모르겠다」였는데 —
497
+ * **눌리는 숫자는 보이는 손잡이가 아니다.** 문은 아래 버튼 칸이 진다.
223
498
  */
224
499
  type: 'string',
225
- name: 'summary',
500
+ name: 'privilegeCount',
226
501
  header: i18next.t('field.privileges'),
227
- record: { align: 'left', renderer: privilegeSummary },
228
- /*
229
- * **여기를 눌러 권한 편집으로 간다.** 컬럼마다 자기 처리기를 두는 것이 관례다
230
- * (`employee-list-page` 가 같은 모양) — 행 전체에 두면 이름을 고치려 누른 것이
231
- * 화면을 떠나 버린다.
232
- */
233
- handlers: {
234
- click: (_columns, _data, _column, record) => {
235
- if (record?.id) {
236
- navigate(`role-privileges/${record.id}`);
237
- }
238
- }
239
- },
240
- width: 300
502
+ record: { align: 'center', renderer: privilegeCountCell },
503
+ width: 110
241
504
  },
242
505
  {
243
- /* 부모 도메인의 역할은 여기서 못 고친다 — 어느 도메인 것인지 먼저 보여야 한다. */
506
+ /*
507
+ * **출처 — 여기가 「보이는 것과 고칠 수 있는 것」을 가른다.**
508
+ *
509
+ * 전에는 도메인 이름을 날값으로 냈다(`SYSTEM` · `MIRATEK`). 그건 구분이 아니다 —
510
+ * 읽는 사람이 자기 도메인 이름을 외우고 있어야 한다. 지금은 「우리」와
511
+ * 「상속 · SYSTEM」 으로 말한다.
512
+ *
513
+ * ⚠ `classifier` 로 행 전체를 물들이지 않는다. 선례들이 색을 `'#8B0000'` 처럼 박아
514
+ * 두었고(dataset 5곳), 그 규약(`emphasized: [배경, 글자]`)이 `var(...)` 를 받는지도
515
+ * 확인되지 않았다. 그리고 `emphasized` 는 **강조**하는 장치인데 여기서 필요한 것은
516
+ * 그 반대다 — 11행 중 9행을 강조하면 강조가 아니다. 그래서 이 칸이 진다.
517
+ */
244
518
  type: 'string',
245
- name: 'domain',
246
- header: i18next.t('field.domain'),
247
- record: { align: 'center', renderer: (_v, _c, record) => record?.domain?.name || '' },
248
- width: 140
519
+ name: 'source',
520
+ header: i18next.t('field.source'),
521
+ record: { align: 'center', renderer: (_v, _c, record) => this.sourceCell(record) },
522
+ width: 150
249
523
  },
250
524
  {
251
525
  type: 'datetime',
@@ -269,10 +543,21 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
269
543
  }
270
544
  }
271
545
  /**
272
- * 역할과 권한을 함께 가져온다.
546
+ * 역할을 가져온다 **권한 행은 받지 않는다.**
547
+ *
548
+ * 전에는 `privileges { id name category }` 를 함께 물었다. 그것으로 그린 것은 잘린 코드 용어
549
+ * 셋이었고, 값은 이랬다.
550
+ *
551
+ * ```
552
+ * [UX 실측] 권한 포함 5,731 bytes · 69ms
553
+ * [UX 실측] 권한 없이 518 bytes · 7ms
554
+ * ```
555
+ *
556
+ * 바이트보다 큰 것은 **`Role.privileges` 필드 리졸버가 역할마다 한 번씩 도는 것**이 함께
557
+ * 없어지는 쪽이다. 지금은 `privilegeCount` 를 받는다 — DB 가 센다(`role-query.ts`).
273
558
  *
274
- * 권한은 `Role.privileges` 필드 리졸버가 역할마다 번씩 읽는다. 역할은 도메인마다 단위이고
275
- * 나누기가 걸려 있으므로 그 비용을 받아들인다 — 요약을 목록에서 보는 값이 그보다 크다.
559
+ * `inherited` 화면이 고른 묶음을 그대로 넘긴다. 기본값이 「우리 역할」이라 처음 화면에
560
+ * 고치는 행이 깔린다.
276
561
  */
277
562
  async fetchHandler({ filters = [], page, limit, sortings = [] }) {
278
563
  this.fetchState = 'loading';
@@ -301,11 +586,7 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
301
586
  name
302
587
  subdomain
303
588
  }
304
- privileges {
305
- id
306
- name
307
- category
308
- }
589
+ privilegeCount
309
590
  updatedAt
310
591
  }
311
592
  total
@@ -316,7 +597,7 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
316
597
  filters,
317
598
  pagination: { page, limit },
318
599
  sortings: sortings.length ? sortings : [{ name: 'name' }],
319
- inherited: InheritedValueType.Include
600
+ inherited: this.scope
320
601
  }
321
602
  })).data.responses;
322
603
  this.total = response.total;
@@ -423,6 +704,55 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
423
704
  }
424
705
  this.grist.fetch();
425
706
  }
707
+ /**
708
+ * 역할을 만드는 두 문을 **한 자리에** 연다.
709
+ *
710
+ * ── 왜 `ox-record-creator`(빈 줄)를 걷어냈나 ────────────────────────────────
711
+ * 이름을 짓는 것은 쉬운 쪽이고 「품질검사원에게 84개 중 무엇을 주나」가 어려운 쪽인데, 빈
712
+ * 줄은 그 판단을 통째로 사람에게 넘긴다. 미라텍이 그래서 역할 둘뿐이다.
713
+ *
714
+ * 그리고 그 단추는 `.filters` 안에 있었는데 `CommonHeaderStyles` 는 `.actions button` 만
715
+ * 꾸민다. `.filters` 에 button 규칙이 없어서 **브라우저 기본 단추**가 그대로 나왔다 — 옆의
716
+ * 알약 범위 단추·검색과 같은 줄에서 혼자 네모난 회색 상자였다. 이 관례를 쓰는 다른 목록
717
+ * 화면(24 파일)도 전부 같은 상태다.
718
+ *
719
+ * ── 순서가 가르친다 ────────────────────────────────────────────────────────
720
+ * 템플릿이 먼저고 백지가 나중이다. 「품질검사원, 권한 7」을 본 사람은 자리가 어떻게 생긴
721
+ * 것인지 알고 나서 자기 것을 짓는다. 그렇다고 백지를 감추면 안 된다 — 「야간반장」·「외주
722
+ * 검사」 같은 자리는 그 공장이 아는 것이지 제품이 아는 게 아니다.
723
+ */
724
+ async openCreate() {
725
+ await import('../../components/role-template-picker.js');
726
+ const popup = openPopup(html `<role-template-picker .page=${this} @close=${() => popup.close()}></role-template-picker>`, {
727
+ backdrop: true,
728
+ size: 'large',
729
+ title: i18next.t('button.create role')
730
+ });
731
+ }
732
+ /** 템플릿에서 — 이름은 이 공장이 정한 것을 그대로 보낸다. */
733
+ async createFromTemplate(templateId, name) {
734
+ const response = await client.mutate({
735
+ mutation: gql `
736
+ mutation ($templateId: String!, $name: String) {
737
+ createRoleFromTemplate(templateId: $templateId, name: $name) {
738
+ id
739
+ }
740
+ }
741
+ `,
742
+ variables: { templateId, name },
743
+ context: gqlContext()
744
+ });
745
+ if (response.errors?.length) {
746
+ document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
747
+ return false;
748
+ }
749
+ this.grist.fetch();
750
+ return true;
751
+ }
752
+ /** 백지에서 — 권한 없이 선다. 목록의 권한 수 칸이 0 을 빨갛게 말하므로 다음 걸음이 보인다. */
753
+ async createBlankRole(name, description) {
754
+ return await this.onCreateRole({ name, description });
755
+ }
426
756
  /**
427
757
  * 새 역할.
428
758
  *
@@ -473,6 +803,14 @@ __decorate([
473
803
  state(),
474
804
  __metadata("design:type", String)
475
805
  ], RoleListPage.prototype, "mySubdomain", void 0);
806
+ __decorate([
807
+ state(),
808
+ __metadata("design:type", String)
809
+ ], RoleListPage.prototype, "scope", void 0);
810
+ __decorate([
811
+ state(),
812
+ __metadata("design:type", Object)
813
+ ], RoleListPage.prototype, "counts", void 0);
476
814
  __decorate([
477
815
  query('ox-grist'),
478
816
  __metadata("design:type", Function)
@@ -481,34 +819,20 @@ RoleListPage = __decorate([
481
819
  customElement('role-list-page')
482
820
  ], RoleListPage);
483
821
  export { RoleListPage };
484
- /** 아는 축은 사람 말로. `role-detail` 과 같은 어휘를 쓴다. */
485
- const AXIS_LABEL = {
486
- query: 'label.read',
487
- mutation: 'label.write',
488
- execute: 'label.execute'
489
- };
490
822
  /**
491
- * `board 읽기·쓰기 · order 읽기` 모듈별로 접어 보여 준다.
823
+ * 권한 **0 가장 할 말이 많은 값이다.**
492
824
  *
493
- * 84개를 적으면 목록이 읽히므로 앞의 셋만 적고 나머지는 수로 말한다. 권한이 하나도 없는
494
- * 역할은 **붙여도 아무 일이 없다** 그것이 조치가 필요한 유일한 상태이므로 색을 쓴다.
825
+ * 권한이 하나도 없는 역할은 붙여도 아무 일이 없다. 그것이 화면에서 **조치가 필요한 유일한
826
+ * 상태**이므로 색을 그때만 쓴다. 나머지는 수만 낸다.
495
827
  */
496
- function privilegeSummary(_value, _column, record) {
497
- const privileges = record?.privileges || [];
498
- if (!privileges.length) {
499
- return html `<span class="badge" attention>${i18next.t('text.no privilege granted')}</span>`;
500
- }
501
- const byCategory = new Map();
502
- for (const { category, name } of privileges) {
503
- const axes = byCategory.get(category) || [];
504
- axes.push(AXIS_LABEL[name] ? i18next.t(AXIS_LABEL[name]) : name);
505
- byCategory.set(category, axes);
828
+ function privilegeCountCell(_value, _column, record) {
829
+ const count = record?.privilegeCount;
830
+ /* 「모른다」와 「0」은 다르다 — 아직 안 온 값에 색을 쓰지 않는다. */
831
+ if (count === undefined || count === null) {
832
+ return html ``;
506
833
  }
507
- const entries = [...byCategory.entries()].sort((a, b) => a[0].localeCompare(b[0]));
508
- const shown = entries.slice(0, 3).map(([category, axes]) => `${category} ${axes.join('·')}`);
509
- const rest = entries.length - shown.length;
510
- return html `<span class="badge"
511
- >${shown.join(' · ')}${rest > 0 ? ` ${i18next.t('text.and x more', { x: rest })}` : ''}</span
512
- >`;
834
+ return count === 0
835
+ ? html `<span class="badge" attention>${i18next.t('text.no privilege granted')}</span>`
836
+ : html `<span class="badge">${count}</span>`;
513
837
  }
514
838
  //# sourceMappingURL=role-list-page.js.map