@things-factory/auth-ui 10.1.4 → 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 +3 -3
  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
@@ -0,0 +1,514 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@operato/data-grist';
4
+ import '@operato/oops/ox-oops-note.js';
5
+ import gql from 'graphql-tag';
6
+ import { css, html, nothing } from 'lit';
7
+ import { customElement, property, query, state } from 'lit/decorators.js';
8
+ import { client, gqlContext } from '@operato/graphql';
9
+ import { OxPrompt } from '@operato/popup/ox-prompt.js';
10
+ import { i18next, localize } from '@operato/i18n';
11
+ import { p13n } from '@operato/p13n';
12
+ import { InheritedValueType, navigate, PageView } from '@operato/shell';
13
+ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles } from '@operato/styles';
14
+ import { isMobileDevice } from '@operato/utils';
15
+ import { getDomain } from '@things-factory/auth-base/dist-client';
16
+ /**
17
+ * 역할 목록 — **무엇을 여는 역할인지 목록에서 읽힌다.**
18
+ *
19
+ * ── 옛 화면이 하던 것 (`pages/role/role-management.ts`) ──────────────────────
20
+ * `quick-find-list` 안에 역할을 늘어놓고, 펼치면 그 안에 이름 입력칸과 84줄의 권한 체크박스가
21
+ * 함께 있었다. 그 구조에서 세 가지가 났다.
22
+ *
23
+ * ① 이름 입력이 매 글자마다 페이지로 올라가 **모든** 편집기에 공유됐다 — A 를 고치고 B 를
24
+ * 저장하면 B 의 이름이 A 가 됐다. 지금은 `role-detail` 이 자기 이름을 들고 있다.
25
+ * ② 목록에 권한이 하나도 안 보였다. 「이 역할이 무엇을 여나」를 알려면 펼쳐야 했다.
26
+ * ③ 한도도 쪽 나누기도 없었다.
27
+ *
28
+ * ── 목록이 말해 주는 것 ─────────────────────────────────────────────────────
29
+ * 역할마다 **몇 개의 모듈에서 무엇이 열려 있나**를 요약해 싣는다(`board 읽기·쓰기 · order 읽기`
30
+ * 식). 사용자 관리에서 역할을 고를 때 이름만 보고 고르던 것이 여기서 갈린다.
31
+ *
32
+ * ── 상속 ────────────────────────────────────────────────────────────────────
33
+ * 부모 도메인의 역할도 함께 나온다(`inherited: Include`) — 실제로 부여할 수 있는 범위가 그것이기
34
+ * 때문이다. 다만 **부모 것은 여기서 못 고친다**(`updateRole` 의 범위가 내 도메인이다). 그래서
35
+ * 어느 도메인의 역할인지 컬럼으로 말한다.
36
+ */
37
+ const RoleListPageBase = p13n(localize(i18next)(PageView));
38
+ let RoleListPage = class RoleListPage extends RoleListPageBase {
39
+ constructor() {
40
+ super(...arguments);
41
+ this.mode = isMobileDevice() ? 'LIST' : 'GRID';
42
+ /* 읽는 중 · 못 읽었다 · 읽었는데 없다 를 가른다. */
43
+ this.fetchState = 'loading';
44
+ this.total = 0;
45
+ this.filtered = false;
46
+ /*
47
+ * 지금 어느 도메인에 있나.
48
+ *
49
+ * 목록은 상속으로 부모 도메인의 역할까지 보여 준다 — 실제로 부여할 수 있는 범위가 그것이기
50
+ * 때문이다. 그런데 **그 행은 여기서 못 고친다**(`updateRole`·`updateMultipleRole` 이
51
+ * `domain.id` 하나로 좁혀 찾는다). 그래서 편집을 열어 두면 안 된다.
52
+ *
53
+ * 프로필의 `domain` 에는 `id` 가 없어서(`auth-private-process-router.ts`) `subdomain` 으로
54
+ * 견준다 — 두 도메인이 같은 subdomain 을 가질 수 없으므로 id 와 같은 답이다.
55
+ */
56
+ this.mySubdomain = '';
57
+ }
58
+ static { this.styles = [
59
+ ScrollbarStyles,
60
+ CommonGristStyles,
61
+ CommonHeaderStyles,
62
+ css `
63
+ :host {
64
+ display: flex;
65
+ flex-direction: column;
66
+ width: 100%;
67
+ background-color: var(--md-sys-color-surface);
68
+ color: var(--md-sys-color-on-surface);
69
+ position: relative;
70
+ }
71
+
72
+ ox-grist {
73
+ flex: 1;
74
+ min-height: 0;
75
+ }
76
+
77
+ /* 요약 배지 — 색은 짝으로, 그리고 「무엇이 나를 기다리나」에만 쓴다. */
78
+ .badge {
79
+ display: inline-block;
80
+ padding: 2px 8px;
81
+ border-radius: var(--md-sys-shape-corner-small, 6px);
82
+ font-size: 0.78rem;
83
+ white-space: nowrap;
84
+ background-color: var(--md-sys-color-surface-variant);
85
+ color: var(--md-sys-color-on-surface-variant);
86
+ }
87
+ .badge[attention] {
88
+ background-color: var(--md-sys-color-error-container);
89
+ color: var(--md-sys-color-on-error-container);
90
+ }
91
+
92
+ ox-oops-note[note] {
93
+ position: absolute;
94
+ inset: 0;
95
+ z-index: 5;
96
+ background-color: var(--md-sys-color-surface);
97
+ }
98
+ `
99
+ ]; }
100
+ /**
101
+ * 저장과 삭제를 **툴바**에 둔다.
102
+ *
103
+ * 격자에서 고치는 화면들의 관례다(`fiscal-quarter-list-page` 외). 격자 안 편집은 여러 줄을
104
+ * 한꺼번에 고치므로, 줄마다 저장 단추를 두면 어디를 눌러야 하는지가 사라진다.
105
+ */
106
+ get context() {
107
+ return {
108
+ title: i18next.t('text.role_management'),
109
+ help: 'auth/roles',
110
+ actions: [
111
+ { title: i18next.t('button.save'), action: this.onSaveGrid.bind(this) },
112
+ { title: i18next.t('button.delete'), action: this.onDeleteGrid.bind(this) }
113
+ ]
114
+ };
115
+ }
116
+ render() {
117
+ return html `
118
+ <ox-grist
119
+ .mode=${this.mode}
120
+ .config=${this.config}
121
+ .fetchHandler=${this.fetchHandler.bind(this)}
122
+ .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')}
123
+ >
124
+ <div slot="headroom" class="header">
125
+ <div class="filters">
126
+ <ox-filters-form autofocus></ox-filters-form>
127
+
128
+ <!-- 목록에서 새 줄을 만드는 관례 (24 파일). 격자 설정에서 칸을 읽어 온다. -->
129
+ <ox-record-creator .callback=${this.onCreateRole.bind(this)}>
130
+ <button><md-icon>add</md-icon></button>
131
+ </ox-record-creator>
132
+ </div>
133
+ <ox-context-page-toolbar class="actions" .context=${this.context}></ox-context-page-toolbar>
134
+ </div>
135
+
136
+ <ox-grist-personalizer slot="setting"></ox-grist-personalizer>
137
+ </ox-grist>
138
+
139
+ ${this.renderNote()}
140
+ `;
141
+ }
142
+ /**
143
+ * 목록 위에 겹쳐 그린다 — 격자를 떼면 다시 읽을 길이 없어진다.
144
+ *
145
+ * **아무 역할도 없는 것이 이 화면의 가장 중요한 자리다.** 새 도메인은 여기서 시작하고,
146
+ * 역할이 없으면 초대한 사람은 로그인해도 볼 화면이 하나도 없다. 그래서 그 사실을 말한다.
147
+ */
148
+ renderNote() {
149
+ if (this.fetchState === 'loading') {
150
+ return nothing;
151
+ }
152
+ if (this.fetchState === 'failed') {
153
+ return html `<ox-oops-note
154
+ note
155
+ icon="error_outline"
156
+ title=${i18next.t('text.failed to fetch')}
157
+ description=${i18next.t('text.please try again')}
158
+ ></ox-oops-note>`;
159
+ }
160
+ if (this.total === 0 && !this.filtered) {
161
+ return html `<ox-oops-note
162
+ note
163
+ icon="badge"
164
+ title=${i18next.t('text.no role yet')}
165
+ description=${i18next.t('text.without a role they see nothing')}
166
+ ></ox-oops-note>`;
167
+ }
168
+ return nothing;
169
+ }
170
+ /**
171
+ * 상속된 역할인가 — **격자의 편집 가드가 이것을 본다.**
172
+ *
173
+ * `mySubdomain` 을 아직 못 읽었으면 **거짓을 돌려주지 않고 상속으로 본다.** 「모른다」를
174
+ * 「내 것이다」로 바꾸면 그 순간 못 고치는 행이 열리고, 사람이 타이핑해 저장한 뒤에 서버가
175
+ * 거절한다. 모를 때는 잠가 두는 쪽이 안전하다.
176
+ */
177
+ isInherited(record) {
178
+ if (!this.mySubdomain) {
179
+ return true;
180
+ }
181
+ const subdomain = record?.domain?.subdomain;
182
+ return Boolean(subdomain) && subdomain !== this.mySubdomain;
183
+ }
184
+ async pageInitialized() {
185
+ this.mySubdomain = (await getDomain())?.subdomain || '';
186
+ this.config = {
187
+ list: {
188
+ fields: ['name', 'description'],
189
+ details: ['summary', 'domain']
190
+ },
191
+ columns: [
192
+ { type: 'gutter', gutterName: 'sequence', fixed: true },
193
+ {
194
+ type: 'string',
195
+ name: 'name',
196
+ header: i18next.t('field.name'),
197
+ /*
198
+ * **격자 안에서 고친다.** 이 저장소의 관례가 그렇고(`editable` 139 파일 대
199
+ * `openPopup` 96 파일), 옛 화면이 이름을 팝업에 두었다가 A 의 이름을 B 에 저장했다.
200
+ *
201
+ * ⚠ **상속된 행은 열지 않는다.** `editable` 이 함수를 받는다
202
+ * (`data-grist` types.ts:458, data-grid-body.js:530·861 이 그렇게 부른다). 이것이
203
+ * 없으면 부모 도메인 역할에 타이핑해 저장할 수 있고, 서버가 그때 거절한다.
204
+ */
205
+ record: { align: 'left', editable: (_v, _c, record) => !this.isInherited(record) },
206
+ filter: 'search',
207
+ sortable: true,
208
+ width: 160
209
+ },
210
+ {
211
+ type: 'string',
212
+ name: 'description',
213
+ header: i18next.t('field.description'),
214
+ record: { align: 'left', editable: (_v, _c, record) => !this.isInherited(record) },
215
+ filter: 'search',
216
+ sortable: true,
217
+ width: 220
218
+ },
219
+ {
220
+ /*
221
+ * **이 역할이 무엇을 여나.** 옛 화면은 이것을 보려면 펼쳐야 했다.
222
+ * 권한이 하나도 없는 역할은 붙여도 아무 일이 없으므로 눈에 걸리게 그린다.
223
+ */
224
+ type: 'string',
225
+ name: 'summary',
226
+ 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
241
+ },
242
+ {
243
+ /* 부모 도메인의 역할은 여기서 못 고친다 — 어느 도메인 것인지 먼저 보여야 한다. */
244
+ 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
249
+ },
250
+ {
251
+ type: 'datetime',
252
+ name: 'updatedAt',
253
+ header: i18next.t('field.updated_at'),
254
+ record: { align: 'center' },
255
+ sortable: true,
256
+ width: 160
257
+ }
258
+ ],
259
+ rows: {
260
+ selectable: { multiple: true }
261
+ },
262
+ sorters: [{ name: 'name' }]
263
+ };
264
+ }
265
+ async pageUpdated(_changes, _lifecycle) {
266
+ if (this.active) {
267
+ await this.updateComplete;
268
+ this.grist.fetch();
269
+ }
270
+ }
271
+ /**
272
+ * 역할과 그 권한을 함께 가져온다.
273
+ *
274
+ * 권한은 `Role.privileges` 필드 리졸버가 역할마다 한 번씩 읽는다. 역할은 도메인마다 열 단위이고
275
+ * 쪽 나누기가 걸려 있으므로 그 비용을 받아들인다 — 요약을 목록에서 보는 값이 그보다 크다.
276
+ */
277
+ async fetchHandler({ filters = [], page, limit, sortings = [] }) {
278
+ this.fetchState = 'loading';
279
+ this.filtered = filters.length > 0;
280
+ try {
281
+ const response = (await client.query({
282
+ query: gql `
283
+ query (
284
+ $filters: [Filter!]
285
+ $pagination: Pagination
286
+ $sortings: [Sorting!]
287
+ $inherited: InheritedValueType
288
+ ) {
289
+ responses: roles(
290
+ filters: $filters
291
+ pagination: $pagination
292
+ sortings: $sortings
293
+ inherited: $inherited
294
+ ) {
295
+ items {
296
+ id
297
+ name
298
+ description
299
+ domain {
300
+ id
301
+ name
302
+ subdomain
303
+ }
304
+ privileges {
305
+ id
306
+ name
307
+ category
308
+ }
309
+ updatedAt
310
+ }
311
+ total
312
+ }
313
+ }
314
+ `,
315
+ variables: {
316
+ filters,
317
+ pagination: { page, limit },
318
+ sortings: sortings.length ? sortings : [{ name: 'name' }],
319
+ inherited: InheritedValueType.Include
320
+ }
321
+ })).data.responses;
322
+ this.total = response.total;
323
+ this.fetchState = 'loaded';
324
+ return {
325
+ records: response.items,
326
+ total: response.total
327
+ };
328
+ }
329
+ catch (e) {
330
+ /* 못 읽은 것을 「없다」로 그리지 않는다. */
331
+ this.fetchState = 'failed';
332
+ throw e;
333
+ }
334
+ }
335
+ /**
336
+ * 격자에서 고친 줄들을 저장한다.
337
+ *
338
+ * 이 저장소의 목록 관례를 그대로 쓴다 — `dirtyRecords` 를 `__dirtyfields__[key].after` 로 펴서
339
+ * `cuFlag` 와 함께 보낸다(`fiscal-quarter-list-page` 가 같은 모양). 서버가
340
+ * `updateMultipleRole` 로 받는다.
341
+ *
342
+ * **`privileges` 는 안 보낸다.** 여기서 고치는 것은 이름과 설명이고, 권한은 전용 화면이
343
+ * 소유한다. 안 보낸 것을 「비우라」로 읽으면 이름만 고친 순간 그 역할이 아무것도 안 여는
344
+ * 역할이 된다 — 서버 쪽에도 같은 말을 주석으로 적어 뒀다.
345
+ */
346
+ async onSaveGrid() {
347
+ const dirty = this.grist.dirtyRecords;
348
+ if (!dirty?.length) {
349
+ return;
350
+ }
351
+ const patches = dirty.map((record) => {
352
+ const patch = record.id ? { id: record.id } : {};
353
+ for (const key in record.__dirtyfields__) {
354
+ patch[key] = record.__dirtyfields__[key].after;
355
+ }
356
+ patch.cuFlag = record.__dirty__;
357
+ return patch;
358
+ });
359
+ const response = await client.mutate({
360
+ mutation: gql `
361
+ mutation ($patches: [RolePatch!]!) {
362
+ updateMultipleRole(patches: $patches) {
363
+ id
364
+ }
365
+ }
366
+ `,
367
+ variables: { patches },
368
+ context: gqlContext()
369
+ });
370
+ if (response.errors?.length) {
371
+ /* 실패를 조용히 넘기지 않는다 — 격자는 고친 채로 남아 있어야 다시 저장할 수 있다. */
372
+ document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
373
+ return;
374
+ }
375
+ this.grist.fetch();
376
+ }
377
+ /**
378
+ * 고른 역할을 지운다.
379
+ *
380
+ * 서버는 **가진 사람이 있는 역할을 남긴다**(`deleteRoles`). 그래서 여기서 「지웠다」고 말하지
381
+ * 않고 목록을 다시 읽는다 — 무엇이 남았는지가 화면에서 보이는 것이 맞다.
382
+ */
383
+ async onDeleteGrid() {
384
+ const selected = this.grist.selected;
385
+ if (!selected?.length) {
386
+ return;
387
+ }
388
+ /*
389
+ * **상속된 행은 여기서 못 지운다** — `deleteRoles` 가 `domain.id` 로 좁혀 찾으므로 그 행은
390
+ * 조용히 남는다. 「지웠다」로 보이고 목록에 그대로 있으면 사람이 두 번 세 번 누른다.
391
+ */
392
+ const mine = selected.filter((record) => !this.isInherited(record));
393
+ if (!mine.length) {
394
+ await OxPrompt.open({
395
+ type: 'warning',
396
+ title: i18next.t('button.delete'),
397
+ text: i18next.t('text.inherited roles are edited in their own domain'),
398
+ confirmButton: { text: i18next.t('button.confirm') }
399
+ });
400
+ return;
401
+ }
402
+ if (!(await OxPrompt.open({
403
+ type: 'warning',
404
+ title: i18next.t('button.delete'),
405
+ text: i18next.t('text.roles in use are kept'),
406
+ confirmButton: { text: i18next.t('button.confirm') },
407
+ cancelButton: { text: i18next.t('button.cancel') }
408
+ }))) {
409
+ return;
410
+ }
411
+ const response = await client.mutate({
412
+ mutation: gql `
413
+ mutation ($ids: [String!]!) {
414
+ deleteRoles(ids: $ids)
415
+ }
416
+ `,
417
+ variables: { ids: mine.map((record) => record.id) },
418
+ context: gqlContext()
419
+ });
420
+ if (response.errors?.length) {
421
+ document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
422
+ return;
423
+ }
424
+ this.grist.fetch();
425
+ }
426
+ /**
427
+ * 새 역할.
428
+ *
429
+ * **권한 없이 만들어진다.** 그것이 맞다 — 이름을 정하는 것과 무엇을 열지 정하는 것은 다른
430
+ * 판단이고, 목록의 요약 칸이 「여는 것이 없음」을 빨갛게 말하므로 다음 걸음이 보인다.
431
+ */
432
+ async onCreateRole(record) {
433
+ const response = await client.mutate({
434
+ mutation: gql `
435
+ mutation ($role: NewRole!) {
436
+ createRole(role: $role) {
437
+ id
438
+ }
439
+ }
440
+ `,
441
+ variables: { role: { name: record.name, description: record.description } },
442
+ context: gqlContext()
443
+ });
444
+ if (response.errors?.length) {
445
+ document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
446
+ return false;
447
+ }
448
+ this.grist.fetch();
449
+ return true;
450
+ }
451
+ };
452
+ __decorate([
453
+ property({ type: Object }),
454
+ __metadata("design:type", Object)
455
+ ], RoleListPage.prototype, "config", void 0);
456
+ __decorate([
457
+ property({ type: String }),
458
+ __metadata("design:type", String)
459
+ ], RoleListPage.prototype, "mode", void 0);
460
+ __decorate([
461
+ state(),
462
+ __metadata("design:type", String)
463
+ ], RoleListPage.prototype, "fetchState", void 0);
464
+ __decorate([
465
+ state(),
466
+ __metadata("design:type", Number)
467
+ ], RoleListPage.prototype, "total", void 0);
468
+ __decorate([
469
+ state(),
470
+ __metadata("design:type", Boolean)
471
+ ], RoleListPage.prototype, "filtered", void 0);
472
+ __decorate([
473
+ state(),
474
+ __metadata("design:type", String)
475
+ ], RoleListPage.prototype, "mySubdomain", void 0);
476
+ __decorate([
477
+ query('ox-grist'),
478
+ __metadata("design:type", Function)
479
+ ], RoleListPage.prototype, "grist", void 0);
480
+ RoleListPage = __decorate([
481
+ customElement('role-list-page')
482
+ ], RoleListPage);
483
+ export { RoleListPage };
484
+ /** 아는 축은 사람 말로. `role-detail` 과 같은 어휘를 쓴다. */
485
+ const AXIS_LABEL = {
486
+ query: 'label.read',
487
+ mutation: 'label.write',
488
+ execute: 'label.execute'
489
+ };
490
+ /**
491
+ * `board 읽기·쓰기 · order 읽기` — 모듈별로 접어 보여 준다.
492
+ *
493
+ * 84개를 다 적으면 목록이 못 읽히므로 앞의 셋만 적고 나머지는 수로 말한다. 권한이 하나도 없는
494
+ * 역할은 **붙여도 아무 일이 없다** — 그것이 조치가 필요한 유일한 상태이므로 색을 쓴다.
495
+ */
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);
506
+ }
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
+ >`;
513
+ }
514
+ //# sourceMappingURL=role-list-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-list-page.js","sourceRoot":"","sources":["../../../client/pages/role/role-list-page.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,qBAAqB,CAAA;AAC5B,OAAO,+BAA+B,CAAA;AAEtC,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AAExC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAEzE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACpC,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACvE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,uCAAuC,CAAA;AAIjE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,gBAAgB,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;AAGnD,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,gBAAgB;IAA3C;;QA6CuB,SAAI,GAA6B,cAAc,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;QAE/F,mCAAmC;QAClB,eAAU,GAAoC,SAAS,CAAA;QACvD,UAAK,GAAW,CAAC,CAAA;QACjB,aAAQ,GAAY,KAAK,CAAA;QAE1C;;;;;;;;;WASG;QACc,gBAAW,GAAW,EAAE,CAAA;IAwY3C,CAAC;aArcQ,WAAM,GAAmB;QAC9B,eAAe;QACf,iBAAiB;QACjB,kBAAkB;QAClB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoCF;KACF,AAzCY,CAyCZ;IAwBD;;;;;OAKG;IACH,IAAI,OAAO;QACT,OAAO;YACL,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;YACxC,IAAI,EAAE,YAAY;YAClB,OAAO,EAAE;gBACP,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;gBACvE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;aAC5E;SACF,CAAA;IACH,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;;gBAEC,IAAI,CAAC,IAAI;kBACP,IAAI,CAAC,MAAM;wBACL,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;kCAClB,IAAI,CAAC,yBAAyB,CAAC,UAAU,CAAE;;;;;;;2CAOlC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;;;;8DAIT,IAAI,CAAC,OAAO;;;;;;QAMlE,IAAI,CAAC,UAAU,EAAE;KACpB,CAAA;IACH,CAAC;IAED;;;;;OAKG;IACK,UAAU;QAChB,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YAClC,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,IAAI,IAAI,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,IAAI,CAAA;;;gBAGD,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;sBAC3B,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;uBACjC,CAAA;QACnB,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACvC,OAAO,IAAI,CAAA;;;gBAGD,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC;sBACvB,OAAO,CAAC,CAAC,CAAC,sCAAsC,CAAC;uBAChD,CAAA;QACnB,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;IAED;;;;;;OAMG;IACK,WAAW,CAAC,MAAW;QAC7B,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,OAAO,IAAI,CAAA;QACb,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,CAAA;QAC3C,OAAO,OAAO,CAAC,SAAS,CAAC,IAAI,SAAS,KAAK,IAAI,CAAC,WAAW,CAAA;IAC7D,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,IAAI,CAAC,WAAW,GAAG,CAAC,MAAM,SAAS,EAAE,CAAC,EAAE,SAAS,IAAI,EAAE,CAAA;QAEvD,IAAI,CAAC,MAAM,GAAG;YACZ,IAAI,EAAE;gBACJ,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC;gBAC/B,OAAO,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC;aAC/B;YACD,OAAO,EAAE;gBACP,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE;gBACvD;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,MAAM;oBACZ,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;oBAC/B;;;;;;;uBAOG;oBACH,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAO,EAAE,EAAO,EAAE,MAAW,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE;oBACjG,MAAM,EAAE,QAAQ;oBAChB,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,aAAa;oBACnB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC;oBACtC,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAO,EAAE,EAAO,EAAE,MAAW,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE;oBACjG,MAAM,EAAE,QAAQ;oBAChB,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE;;;uBAGG;oBACH,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,SAAS;oBACf,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC;oBACrC,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE;oBACrD;;;;uBAIG;oBACH,QAAQ,EAAE;wBACR,KAAK,EAAE,CAAC,QAAa,EAAE,KAAU,EAAE,OAAY,EAAE,MAAW,EAAE,EAAE;4BAC9D,IAAI,MAAM,EAAE,EAAE,EAAE,CAAC;gCACf,QAAQ,CAAC,mBAAmB,MAAM,CAAC,EAAE,EAAE,CAAC,CAAA;4BAC1C,CAAC;wBACH,CAAC;qBACF;oBACD,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,mDAAmD;oBACnD,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,QAAQ;oBACd,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;oBACjC,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAO,EAAE,EAAO,EAAE,MAAW,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,EAAE;oBACpG,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,UAAU;oBAChB,IAAI,EAAE,WAAW;oBACjB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC;oBACrC,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE;oBAC3B,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;aACF;YACD,IAAI,EAAE;gBACJ,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;aAC/B;YACD,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;SAC5B,CAAA;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,QAAa,EAAE,UAAe;QAC9C,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,CAAC,cAAc,CAAA;YACzB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QACpB,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,GAAG,EAAE,EAAe;QAC1E,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;QAC3B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAA;QAElC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,CACf,MAAM,MAAM,CAAC,KAAK,CAAC;gBACjB,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAgCT;gBACD,SAAS,EAAE;oBACT,OAAO;oBACP,UAAU,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;oBAC3B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;oBACzD,SAAS,EAAE,kBAAkB,CAAC,OAAO;iBACtC;aACF,CAAC,CACH,CAAC,IAAI,CAAC,SAAS,CAAA;YAEhB,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAA;YAC3B,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAA;YAE1B,OAAO;gBACL,OAAO,EAAE,QAAQ,CAAC,KAAK;gBACvB,KAAK,EAAE,QAAQ,CAAC,KAAK;aACtB,CAAA;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,4BAA4B;YAC5B,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAA;YAC1B,MAAM,CAAC,CAAA;QACT,CAAC;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACK,KAAK,CAAC,UAAU;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAA;QACrC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;YACnB,OAAM;QACR,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,MAAW,EAAE,EAAE;YACxC,MAAM,KAAK,GAAQ,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;YACrD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;gBACzC,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;YAChD,CAAC;YACD,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,SAAS,CAAA;YAC/B,OAAO,KAAK,CAAA;QACd,CAAC,CAAC,CAAA;QAEF,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;OAMZ;YACD,SAAS,EAAE,EAAE,OAAO,EAAE;YACtB,OAAO,EAAE,UAAU,EAAE;SACtB,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;YAC5B,qDAAqD;YACrD,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,CAChG,CAAA;YACD,OAAM;QACR,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;IACpB,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,YAAY;QACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAA;QACpC,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;YACtB,OAAM;QACR,CAAC;QAED;;;WAGG;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAW,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;QACxE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,QAAQ,CAAC,IAAI,CAAC;gBAClB,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;gBACjC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,qDAAqD,CAAC;gBACtE,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE;aACrD,CAAC,CAAA;YACF,OAAM;QACR,CAAC;QAED,IACE,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC;YACpB,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;YACjC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,4BAA4B,CAAC;YAC7C,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE;YACpD,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE;SACnD,CAAC,CAAC,EACH,CAAC;YACD,OAAM;QACR,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;;;OAIZ;YACD,SAAS,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,MAAW,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE;YACxD,OAAO,EAAE,UAAU,EAAE;SACtB,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;YAC5B,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,CAChG,CAAA;YACD,OAAM;QACR,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;IACpB,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,YAAY,CAAC,MAAW;QACpC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;OAMZ;YACD,SAAS,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,EAAE;YAC3E,OAAO,EAAE,UAAU,EAAE;SACtB,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;YAC5B,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,CAChG,CAAA;YACD,OAAO,KAAK,CAAA;QACd,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QAClB,OAAO,IAAI,CAAA;IACb,CAAC;;AAzZ2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;4CAAY;AACX;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;0CAAoE;AAG9E;IAAhB,KAAK,EAAE;;gDAAgE;AACvD;IAAhB,KAAK,EAAE;;2CAA0B;AACjB;IAAhB,KAAK,EAAE;;8CAAkC;AAYzB;IAAhB,KAAK,EAAE;;iDAAiC;AAEd;IAA1B,KAAK,CAAC,UAAU,CAAC;;2CAA0B;AAhEjC,YAAY;IADxB,aAAa,CAAC,gBAAgB,CAAC;GACnB,YAAY,CAscxB;;AAED,8CAA8C;AAC9C,MAAM,UAAU,GAA2B;IACzC,KAAK,EAAE,YAAY;IACnB,QAAQ,EAAE,aAAa;IACvB,OAAO,EAAE,eAAe;CACzB,CAAA;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,MAAW,EAAE,OAAY,EAAE,MAAW;IAC9D,MAAM,UAAU,GAAG,MAAM,EAAE,UAAU,IAAI,EAAE,CAAA;IAE3C,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;QACvB,OAAO,IAAI,CAAA,iCAAiC,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC,SAAS,CAAA;IAC7F,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC9C,KAAK,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,UAAU,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;QAC3C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QAChE,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAChC,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAClF,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,GAAG,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC5F,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAA;IAE1C,OAAO,IAAI,CAAA;OACN,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,iBAAiB,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;IACtF,CAAA;AACJ,CAAC","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/data-grist'\nimport '@operato/oops/ox-oops-note.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, nothing } from 'lit'\nimport type { CSSResultGroup } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { i18next, localize } from '@operato/i18n'\nimport { p13n } from '@operato/p13n'\nimport { InheritedValueType, navigate, PageView } from '@operato/shell'\nimport { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { getDomain } from '@things-factory/auth-base/dist-client'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\n\n/**\n * 역할 목록 — **무엇을 여는 역할인지 목록에서 읽힌다.**\n *\n * ── 옛 화면이 하던 것 (`pages/role/role-management.ts`) ──────────────────────\n * `quick-find-list` 안에 역할을 늘어놓고, 펼치면 그 안에 이름 입력칸과 84줄의 권한 체크박스가\n * 함께 있었다. 그 구조에서 세 가지가 났다.\n *\n * ① 이름 입력이 매 글자마다 페이지로 올라가 **모든** 편집기에 공유됐다 — A 를 고치고 B 를\n * 저장하면 B 의 이름이 A 가 됐다. 지금은 `role-detail` 이 자기 이름을 들고 있다.\n * ② 목록에 권한이 하나도 안 보였다. 「이 역할이 무엇을 여나」를 알려면 펼쳐야 했다.\n * ③ 한도도 쪽 나누기도 없었다.\n *\n * ── 목록이 말해 주는 것 ─────────────────────────────────────────────────────\n * 역할마다 **몇 개의 모듈에서 무엇이 열려 있나**를 요약해 싣는다(`board 읽기·쓰기 · order 읽기`\n * 식). 사용자 관리에서 역할을 고를 때 이름만 보고 고르던 것이 여기서 갈린다.\n *\n * ── 상속 ────────────────────────────────────────────────────────────────────\n * 부모 도메인의 역할도 함께 나온다(`inherited: Include`) — 실제로 부여할 수 있는 범위가 그것이기\n * 때문이다. 다만 **부모 것은 여기서 못 고친다**(`updateRole` 의 범위가 내 도메인이다). 그래서\n * 어느 도메인의 역할인지 컬럼으로 말한다.\n */\nconst RoleListPageBase = p13n(localize(i18next)(PageView))\n\n@customElement('role-list-page')\nexport class RoleListPage extends RoleListPageBase {\n static styles: CSSResultGroup = [\n ScrollbarStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n width: 100%;\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n position: relative;\n }\n\n ox-grist {\n flex: 1;\n min-height: 0;\n }\n\n /* 요약 배지 — 색은 짝으로, 그리고 「무엇이 나를 기다리나」에만 쓴다. */\n .badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: var(--md-sys-shape-corner-small, 6px);\n font-size: 0.78rem;\n white-space: nowrap;\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n }\n .badge[attention] {\n background-color: var(--md-sys-color-error-container);\n color: var(--md-sys-color-on-error-container);\n }\n\n ox-oops-note[note] {\n position: absolute;\n inset: 0;\n z-index: 5;\n background-color: var(--md-sys-color-surface);\n }\n `\n ]\n\n @property({ type: Object }) config: any\n @property({ type: String }) mode: 'CARD' | 'GRID' | 'LIST' = isMobileDevice() ? 'LIST' : 'GRID'\n\n /* 읽는 중 · 못 읽었다 · 읽었는데 없다 를 가른다. */\n @state() private fetchState: 'loading' | 'failed' | 'loaded' = 'loading'\n @state() private total: number = 0\n @state() private filtered: boolean = false\n\n /*\n * 지금 어느 도메인에 있나.\n *\n * 목록은 상속으로 부모 도메인의 역할까지 보여 준다 — 실제로 부여할 수 있는 범위가 그것이기\n * 때문이다. 그런데 **그 행은 여기서 못 고친다**(`updateRole`·`updateMultipleRole` 이\n * `domain.id` 하나로 좁혀 찾는다). 그래서 편집을 열어 두면 안 된다.\n *\n * 프로필의 `domain` 에는 `id` 가 없어서(`auth-private-process-router.ts`) `subdomain` 으로\n * 견준다 — 두 도메인이 같은 subdomain 을 가질 수 없으므로 id 와 같은 답이다.\n */\n @state() private mySubdomain: string = ''\n\n @query('ox-grist') private grist!: DataGrist\n\n /**\n * 저장과 삭제를 **툴바**에 둔다.\n *\n * 격자에서 고치는 화면들의 관례다(`fiscal-quarter-list-page` 외). 격자 안 편집은 여러 줄을\n * 한꺼번에 고치므로, 줄마다 저장 단추를 두면 어디를 눌러야 하는지가 사라진다.\n */\n get context() {\n return {\n title: i18next.t('text.role_management'),\n help: 'auth/roles',\n actions: [\n { title: i18next.t('button.save'), action: this.onSaveGrid.bind(this) },\n { title: i18next.t('button.delete'), action: this.onDeleteGrid.bind(this) }\n ]\n }\n }\n\n render() {\n return html`\n <ox-grist\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <div class=\"filters\">\n <ox-filters-form autofocus></ox-filters-form>\n\n <!-- 목록에서 새 줄을 만드는 관례 (24 파일). 격자 설정에서 칸을 읽어 온다. -->\n <ox-record-creator .callback=${this.onCreateRole.bind(this)}>\n <button><md-icon>add</md-icon></button>\n </ox-record-creator>\n </div>\n <ox-context-page-toolbar class=\"actions\" .context=${this.context}></ox-context-page-toolbar>\n </div>\n\n <ox-grist-personalizer slot=\"setting\"></ox-grist-personalizer>\n </ox-grist>\n\n ${this.renderNote()}\n `\n }\n\n /**\n * 목록 위에 겹쳐 그린다 — 격자를 떼면 다시 읽을 길이 없어진다.\n *\n * **아무 역할도 없는 것이 이 화면의 가장 중요한 자리다.** 새 도메인은 여기서 시작하고,\n * 역할이 없으면 초대한 사람은 로그인해도 볼 화면이 하나도 없다. 그래서 그 사실을 말한다.\n */\n private renderNote() {\n if (this.fetchState === 'loading') {\n return nothing\n }\n\n if (this.fetchState === 'failed') {\n return html`<ox-oops-note\n note\n icon=\"error_outline\"\n title=${i18next.t('text.failed to fetch')}\n description=${i18next.t('text.please try again')}\n ></ox-oops-note>`\n }\n\n if (this.total === 0 && !this.filtered) {\n return html`<ox-oops-note\n note\n icon=\"badge\"\n title=${i18next.t('text.no role yet')}\n description=${i18next.t('text.without a role they see nothing')}\n ></ox-oops-note>`\n }\n\n return nothing\n }\n\n /**\n * 상속된 역할인가 — **격자의 편집 가드가 이것을 본다.**\n *\n * `mySubdomain` 을 아직 못 읽었으면 **거짓을 돌려주지 않고 상속으로 본다.** 「모른다」를\n * 「내 것이다」로 바꾸면 그 순간 못 고치는 행이 열리고, 사람이 타이핑해 저장한 뒤에 서버가\n * 거절한다. 모를 때는 잠가 두는 쪽이 안전하다.\n */\n private isInherited(record: any): boolean {\n if (!this.mySubdomain) {\n return true\n }\n\n const subdomain = record?.domain?.subdomain\n return Boolean(subdomain) && subdomain !== this.mySubdomain\n }\n\n async pageInitialized() {\n this.mySubdomain = (await getDomain())?.subdomain || ''\n\n this.config = {\n list: {\n fields: ['name', 'description'],\n details: ['summary', 'domain']\n },\n columns: [\n { type: 'gutter', gutterName: 'sequence', fixed: true },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('field.name'),\n /*\n * **격자 안에서 고친다.** 이 저장소의 관례가 그렇고(`editable` 139 파일 대\n * `openPopup` 96 파일), 옛 화면이 이름을 팝업에 두었다가 A 의 이름을 B 에 저장했다.\n *\n * ⚠ **상속된 행은 열지 않는다.** `editable` 이 함수를 받는다\n * (`data-grist` types.ts:458, data-grid-body.js:530·861 이 그렇게 부른다). 이것이\n * 없으면 부모 도메인 역할에 타이핑해 저장할 수 있고, 서버가 그때 거절한다.\n */\n record: { align: 'left', editable: (_v: any, _c: any, record: any) => !this.isInherited(record) },\n filter: 'search',\n sortable: true,\n width: 160\n },\n {\n type: 'string',\n name: 'description',\n header: i18next.t('field.description'),\n record: { align: 'left', editable: (_v: any, _c: any, record: any) => !this.isInherited(record) },\n filter: 'search',\n sortable: true,\n width: 220\n },\n {\n /*\n * **이 역할이 무엇을 여나.** 옛 화면은 이것을 보려면 펼쳐야 했다.\n * 권한이 하나도 없는 역할은 붙여도 아무 일이 없으므로 눈에 걸리게 그린다.\n */\n type: 'string',\n name: 'summary',\n header: i18next.t('field.privileges'),\n record: { align: 'left', renderer: privilegeSummary },\n /*\n * **여기를 눌러 권한 편집으로 간다.** 컬럼마다 자기 처리기를 두는 것이 관례다\n * (`employee-list-page` 가 같은 모양) — 행 전체에 두면 이름을 고치려 누른 것이\n * 화면을 떠나 버린다.\n */\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: any) => {\n if (record?.id) {\n navigate(`role-privileges/${record.id}`)\n }\n }\n },\n width: 300\n },\n {\n /* 부모 도메인의 역할은 여기서 못 고친다 — 어느 도메인 것인지 먼저 보여야 한다. */\n type: 'string',\n name: 'domain',\n header: i18next.t('field.domain'),\n record: { align: 'center', renderer: (_v: any, _c: any, record: any) => record?.domain?.name || '' },\n width: 140\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated_at'),\n record: { align: 'center' },\n sortable: true,\n width: 160\n }\n ],\n rows: {\n selectable: { multiple: true }\n },\n sorters: [{ name: 'name' }]\n }\n }\n\n async pageUpdated(_changes: any, _lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n }\n }\n\n /**\n * 역할과 그 권한을 함께 가져온다.\n *\n * 권한은 `Role.privileges` 필드 리졸버가 역할마다 한 번씩 읽는다. 역할은 도메인마다 열 단위이고\n * 쪽 나누기가 걸려 있으므로 그 비용을 받아들인다 — 요약을 목록에서 보는 값이 그보다 크다.\n */\n async fetchHandler({ filters = [], page, limit, sortings = [] }: FetchOption) {\n this.fetchState = 'loading'\n this.filtered = filters.length > 0\n\n try {\n const response = (\n await client.query({\n query: gql`\n query (\n $filters: [Filter!]\n $pagination: Pagination\n $sortings: [Sorting!]\n $inherited: InheritedValueType\n ) {\n responses: roles(\n filters: $filters\n pagination: $pagination\n sortings: $sortings\n inherited: $inherited\n ) {\n items {\n id\n name\n description\n domain {\n id\n name\n subdomain\n }\n privileges {\n id\n name\n category\n }\n updatedAt\n }\n total\n }\n }\n `,\n variables: {\n filters,\n pagination: { page, limit },\n sortings: sortings.length ? sortings : [{ name: 'name' }],\n inherited: InheritedValueType.Include\n }\n })\n ).data.responses\n\n this.total = response.total\n this.fetchState = 'loaded'\n\n return {\n records: response.items,\n total: response.total\n }\n } catch (e) {\n /* 못 읽은 것을 「없다」로 그리지 않는다. */\n this.fetchState = 'failed'\n throw e\n }\n }\n\n /**\n * 격자에서 고친 줄들을 저장한다.\n *\n * 이 저장소의 목록 관례를 그대로 쓴다 — `dirtyRecords` 를 `__dirtyfields__[key].after` 로 펴서\n * `cuFlag` 와 함께 보낸다(`fiscal-quarter-list-page` 가 같은 모양). 서버가\n * `updateMultipleRole` 로 받는다.\n *\n * **`privileges` 는 안 보낸다.** 여기서 고치는 것은 이름과 설명이고, 권한은 전용 화면이\n * 소유한다. 안 보낸 것을 「비우라」로 읽으면 이름만 고친 순간 그 역할이 아무것도 안 여는\n * 역할이 된다 — 서버 쪽에도 같은 말을 주석으로 적어 뒀다.\n */\n private async onSaveGrid() {\n const dirty = this.grist.dirtyRecords\n if (!dirty?.length) {\n return\n }\n\n const patches = dirty.map((record: any) => {\n const patch: any = record.id ? { id: record.id } : {}\n for (const key in record.__dirtyfields__) {\n patch[key] = record.__dirtyfields__[key].after\n }\n patch.cuFlag = record.__dirty__\n return patch\n })\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($patches: [RolePatch!]!) {\n updateMultipleRole(patches: $patches) {\n id\n }\n }\n `,\n variables: { patches },\n context: gqlContext()\n })\n\n if (response.errors?.length) {\n /* 실패를 조용히 넘기지 않는다 — 격자는 고친 채로 남아 있어야 다시 저장할 수 있다. */\n document.dispatchEvent(\n new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } })\n )\n return\n }\n\n this.grist.fetch()\n }\n\n /**\n * 고른 역할을 지운다.\n *\n * 서버는 **가진 사람이 있는 역할을 남긴다**(`deleteRoles`). 그래서 여기서 「지웠다」고 말하지\n * 않고 목록을 다시 읽는다 — 무엇이 남았는지가 화면에서 보이는 것이 맞다.\n */\n private async onDeleteGrid() {\n const selected = this.grist.selected\n if (!selected?.length) {\n return\n }\n\n /*\n * **상속된 행은 여기서 못 지운다** — `deleteRoles` 가 `domain.id` 로 좁혀 찾으므로 그 행은\n * 조용히 남는다. 「지웠다」로 보이고 목록에 그대로 있으면 사람이 두 번 세 번 누른다.\n */\n const mine = selected.filter((record: any) => !this.isInherited(record))\n if (!mine.length) {\n await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.delete'),\n text: i18next.t('text.inherited roles are edited in their own domain'),\n confirmButton: { text: i18next.t('button.confirm') }\n })\n return\n }\n\n if (\n !(await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.delete'),\n text: i18next.t('text.roles in use are kept'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n }))\n ) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($ids: [String!]!) {\n deleteRoles(ids: $ids)\n }\n `,\n variables: { ids: mine.map((record: any) => record.id) },\n context: gqlContext()\n })\n\n if (response.errors?.length) {\n document.dispatchEvent(\n new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } })\n )\n return\n }\n\n this.grist.fetch()\n }\n\n /**\n * 새 역할.\n *\n * **권한 없이 만들어진다.** 그것이 맞다 — 이름을 정하는 것과 무엇을 열지 정하는 것은 다른\n * 판단이고, 목록의 요약 칸이 「여는 것이 없음」을 빨갛게 말하므로 다음 걸음이 보인다.\n */\n private async onCreateRole(record: any) {\n const response = await client.mutate({\n mutation: gql`\n mutation ($role: NewRole!) {\n createRole(role: $role) {\n id\n }\n }\n `,\n variables: { role: { name: record.name, description: record.description } },\n context: gqlContext()\n })\n\n if (response.errors?.length) {\n document.dispatchEvent(\n new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } })\n )\n return false\n }\n\n this.grist.fetch()\n return true\n }\n}\n\n/** 아는 축은 사람 말로. `role-detail` 과 같은 어휘를 쓴다. */\nconst AXIS_LABEL: Record<string, string> = {\n query: 'label.read',\n mutation: 'label.write',\n execute: 'label.execute'\n}\n\n/**\n * `board 읽기·쓰기 · order 읽기` — 모듈별로 접어 보여 준다.\n *\n * 84개를 다 적으면 목록이 못 읽히므로 앞의 셋만 적고 나머지는 수로 말한다. 권한이 하나도 없는\n * 역할은 **붙여도 아무 일이 없다** — 그것이 조치가 필요한 유일한 상태이므로 색을 쓴다.\n */\nfunction privilegeSummary(_value: any, _column: any, record: any) {\n const privileges = record?.privileges || []\n\n if (!privileges.length) {\n return html`<span class=\"badge\" attention>${i18next.t('text.no privilege granted')}</span>`\n }\n\n const byCategory = new Map<string, string[]>()\n for (const { category, name } of privileges) {\n const axes = byCategory.get(category) || []\n axes.push(AXIS_LABEL[name] ? i18next.t(AXIS_LABEL[name]) : name)\n byCategory.set(category, axes)\n }\n\n const entries = [...byCategory.entries()].sort((a, b) => a[0].localeCompare(b[0]))\n const shown = entries.slice(0, 3).map(([category, axes]) => `${category} ${axes.join('·')}`)\n const rest = entries.length - shown.length\n\n return html`<span class=\"badge\"\n >${shown.join(' · ')}${rest > 0 ? ` ${i18next.t('text.and x more', { x: rest })}` : ''}</span\n >`\n}\n"]}