@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.
- package/dist-client/components/role-privilege-picker-state.d.ts +11 -0
- package/dist-client/components/role-privilege-picker-state.js +15 -0
- package/dist-client/components/role-privilege-picker-state.js.map +1 -0
- package/dist-client/components/role-privilege-picker-state.test.js +20 -0
- package/dist-client/components/role-privilege-picker-state.test.js.map +1 -0
- package/dist-client/components/role-privilege-picker.d.ts +40 -0
- package/dist-client/components/role-privilege-picker.js +77 -0
- package/dist-client/components/role-privilege-picker.js.map +1 -0
- package/dist-client/components/role-template-picker.d.ts +83 -0
- package/dist-client/components/role-template-picker.js +487 -0
- package/dist-client/components/role-template-picker.js.map +1 -0
- package/dist-client/pages/role/role-list-page.d.ts +104 -13
- package/dist-client/pages/role/role-list-page.js +396 -72
- package/dist-client/pages/role/role-list-page.js.map +1 -1
- package/dist-client/pages/role/role-privilege-page.d.ts +113 -37
- package/dist-client/pages/role/role-privilege-page.js +426 -139
- package/dist-client/pages/role/role-privilege-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/translations/en.json +27 -1
- package/translations/ja.json +14 -1
- package/translations/ko.json +26 -0
- package/translations/ms.json +15 -2
- package/translations/zh.json +14 -1
- package/dist-client/components/create-role.d.ts +0 -1
- package/dist-client/components/create-role.js +0 -117
- package/dist-client/components/create-role.js.map +0 -1
- package/dist-client/components/credential-manager.d.ts +0 -11
- package/dist-client/components/credential-manager.js +0 -64
- package/dist-client/components/credential-manager.js.map +0 -1
- package/dist-client/components/role-detail.d.ts +0 -144
- package/dist-client/components/role-detail.js +0 -698
- package/dist-client/components/role-detail.js.map +0 -1
- package/dist-client/components/role-edit-form.js +0 -95
- package/dist-client/components/role-edit-form.js.map +0 -1
- package/dist-client/components/role-privilege-editor.d.ts +0 -1
- package/dist-client/components/role-privilege-editor.js +0 -368
- package/dist-client/components/role-privilege-editor.js.map +0 -1
- package/dist-client/pages/role/role-management.d.ts +0 -4
- package/dist-client/pages/role/role-management.js +0 -137
- package/dist-client/pages/role/role-management.js.map +0 -1
- package/dist-client/pages/user/user-management.d.ts +0 -35
- package/dist-client/pages/user/user-management.js +0 -223
- package/dist-client/pages/user/user-management.js.map +0 -1
- /package/dist-client/components/{role-edit-form.d.ts → role-privilege-picker-state.test.d.ts} +0 -0
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import '@material/web/icon/icon.js';
|
|
3
|
-
import '@
|
|
4
|
-
import '@material/web/checkbox/checkbox.js';
|
|
3
|
+
import '@operato/input/ox-input-search.js';
|
|
5
4
|
import '@operato/oops/ox-oops-note.js';
|
|
6
5
|
import gql from 'graphql-tag';
|
|
7
6
|
import { css, html, nothing } from 'lit';
|
|
8
7
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
9
8
|
import { client, gqlContext } from '@operato/graphql';
|
|
10
9
|
import { i18next, localize } from '@operato/i18n';
|
|
10
|
+
import { openPopup } from '@operato/layout';
|
|
11
|
+
import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
11
12
|
import { PageView } from '@operato/shell';
|
|
12
13
|
import { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles';
|
|
13
14
|
import { getDomain } from '@things-factory/auth-base/dist-client';
|
|
15
|
+
import { filterPrivilegeModules, selectedVisibleModule } from '../../components/role-privilege-picker-state.js';
|
|
14
16
|
/** 아는 축은 사람 말로 쓰고, 그 순서로 세운다. 모르는 축은 자기 이름으로 뒤에 붙는다. */
|
|
15
17
|
const KNOWN_AXES = ['query', 'mutation', 'execute'];
|
|
16
18
|
const AXIS_LABEL = {
|
|
@@ -19,35 +21,33 @@ const AXIS_LABEL = {
|
|
|
19
21
|
execute: 'label.execute'
|
|
20
22
|
};
|
|
21
23
|
/**
|
|
22
|
-
* 한 역할이 무엇을
|
|
24
|
+
* 한 역할이 무엇을 할 수 있나 — **켜진 것이 문장으로 본문에 선다.**
|
|
23
25
|
*
|
|
24
|
-
* ──
|
|
25
|
-
*
|
|
26
|
-
* 것으로 답했는데 그것이 증상 처리였다 — **모듈이 46개고 각 줄에 설명 문장이 붙으면 팝업에
|
|
27
|
-
* 안 들어간다.** 자리 자체가 틀렸다.
|
|
26
|
+
* ── 사용자 평가가 이 화면을 두 번 뒤집었다 ──────────────────────────────────
|
|
27
|
+
* **「화면으로부터 배울 수 있는 것이 없고, 혼란스럽고, 어렵고, 불편하다.」**
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* 처음에는 팝업 안의 `모듈 × 읽기·쓰기` 격자였다. 격자는 「이 모듈에서 무엇이 켜졌나」에
|
|
30
|
+
* 답하는데, 관리자가 묻는 것은 **「이 역할이 무엇을 하나」**다. 격자로 그걸 읽으려면 모듈
|
|
31
|
+
* 46개를 한 번씩 열어야 하고, 역할을 부여하는 사람은 부여하기 **전에** 그 답이 필요하다.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* 옛 격자는 `board` 라고만 쓰고 「쓰기를 켜면 이 사람이 무엇을 할 수 있게 되나」를 어디에도 안
|
|
36
|
-
* 적었다. 설명을 체크박스 `title` 에 숨겨 놨는데 마우스를 올려야 나오고, 그건 배우는 길이 아니다.
|
|
33
|
+
* 그래서 뒤집었다(UX 레인 §2). 켜진 것을 문장으로 늘어놓는 것이 본문이고, 격자는 **더할 때
|
|
34
|
+
* 쓰는 도구**로 자리를 옮겼다(`renderAdd`). 버린 것은 없다.
|
|
37
35
|
*
|
|
38
|
-
*
|
|
36
|
+
* ── 문장은 어디서 오나 ──────────────────────────────────────────────────────
|
|
37
|
+
* ```
|
|
38
|
+
* 1 Privilege.description 서버가 채우면 그것이 이긴다 — 배포마다 다를 수 있는 사실은 서버가 안다
|
|
39
|
+
* 2 privilege.allows.<category>.<axis> UX 레인이 쓴 글 (57 모듈 · 104 문장, ko·en)
|
|
40
|
+
* 3 「아직 설명이 없습니다」 둘 다 없으면. 지어내지 않는다
|
|
41
|
+
* ```
|
|
39
42
|
*
|
|
40
|
-
*
|
|
41
|
-
* `
|
|
42
|
-
* (`auth-base/server/index.ts:34-36`) 그 mutation 을 부르는 화면이 없다 — 713개 선언이 전부
|
|
43
|
-
* 빈 설명이다. 그래서 이 화면은 **없으면 없다고 말한다.** 코드 낱말(`board mutation`)을 다시
|
|
44
|
-
* 보여 주지 않고, 지어내지도 않는다. 글은 UX 레인이 `privilege.category.*` 키로 채우는 중이다.
|
|
43
|
+
* 1번은 아직 **전부 비어 있다** — 부팅 동기화가 `{ category, name }` 둘만 저장한다
|
|
44
|
+
* (`auth-base/server/index.ts:34-36`). 그래서 지금 화면에 나오는 문장은 전부 2번이다.
|
|
45
45
|
*
|
|
46
46
|
* ── 저장 범위는 서버가 준 목록이다 ──────────────────────────────────────────
|
|
47
|
-
* 옛 편집기는 `querySelectorAll('input:checked')` 로 보냈다.
|
|
48
|
-
* 앞 모듈의 체크박스는 **DOM 에 없다** — 그 방식이면 모듈을 옮겨 다니는 것만으로
|
|
49
|
-
* 조용히 떨어진다. 여기서는 `rolePrivileges` 가 준 전체가
|
|
50
|
-
* 있다. 화면이 무엇을 그렸는지와 무관하다.
|
|
47
|
+
* 옛 편집기는 `querySelectorAll('input:checked')` 로 보냈다. 「더하기」 판에서 다른 모듈로
|
|
48
|
+
* 옮기면 앞 모듈의 체크박스는 **DOM 에 없다** — 그 방식이면 모듈을 옮겨 다니는 것만으로
|
|
49
|
+
* 권한이 조용히 떨어진다. 여기서는 `rolePrivileges` 가 준 전체가 범위이고 켠 것은
|
|
50
|
+
* `assigned` 에만 있다. 화면이 무엇을 그렸는지와 무관하다.
|
|
51
51
|
*/
|
|
52
52
|
let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageView) {
|
|
53
53
|
constructor() {
|
|
@@ -65,6 +65,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
65
65
|
this.mySubdomain = '';
|
|
66
66
|
this.fetchState = 'loading';
|
|
67
67
|
this.saving = false;
|
|
68
|
+
this.dirty = false;
|
|
68
69
|
}
|
|
69
70
|
static { this.styles = [
|
|
70
71
|
ScrollbarStyles,
|
|
@@ -128,11 +129,19 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
/*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
걸러 보는 칸은 ox-input-search 다.
|
|
133
|
+
|
|
134
|
+
여기 md-filled-text-field 를 썼다가 사용자에게 「전체적인 스타일에 어울리지 않는다」를
|
|
135
|
+
들었다. 그것은 회색 채움 상자에 라벨이 떠 있는 **폼 칸**이고, 이 자리는 **찾는 칸**이다.
|
|
136
|
+
|
|
137
|
+
근거는 이 화면 자신에 있다 — 역할 목록의 검색 칸이 이 부품이다.
|
|
138
|
+
[읽었음] @operato/data-grist/dist/src/filters/filters-form.js:153
|
|
139
|
+
|
|
140
|
+
⚠ 폼 칸도 md-* 를 안 쓴다 — 사용자가 2026-09-11 에 역할 화면 전체로 못 박았다
|
|
141
|
+
(「너무 크고 스타일이 안 어울린다」). native input 에 FormFieldStyles 를 얹는다.
|
|
142
|
+
그 블록이 정확히 이 일을 하려고 있는 것이다(shadow 경계를 넘는 토큰 연결).
|
|
134
143
|
*/
|
|
135
|
-
|
|
144
|
+
ox-input-search {
|
|
136
145
|
margin: 8px 12px;
|
|
137
146
|
width: calc(100% - 24px);
|
|
138
147
|
}
|
|
@@ -192,6 +201,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
192
201
|
gap: 4px 10px;
|
|
193
202
|
padding: 12px 14px;
|
|
194
203
|
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
|
204
|
+
cursor: pointer;
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
ul[axes] li:last-child {
|
|
@@ -208,6 +218,39 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
208
218
|
cursor: default;
|
|
209
219
|
}
|
|
210
220
|
|
|
221
|
+
ul[axes] li[ghost],
|
|
222
|
+
ul[axes] li[readonly] {
|
|
223
|
+
cursor: default;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
[picker-footer] {
|
|
227
|
+
display: flex;
|
|
228
|
+
align-items: center;
|
|
229
|
+
justify-content: space-between;
|
|
230
|
+
gap: var(--spacing-medium, 8px);
|
|
231
|
+
padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
232
|
+
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
233
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
234
|
+
font-size: 0.85rem;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
[picker-footer] button {
|
|
238
|
+
flex: 0 0 auto;
|
|
239
|
+
padding: 8px 16px;
|
|
240
|
+
border: 0;
|
|
241
|
+
border-radius: var(--md-sys-shape-corner-small, 6px);
|
|
242
|
+
color: var(--md-sys-color-on-primary);
|
|
243
|
+
background-color: var(--md-sys-color-primary);
|
|
244
|
+
font: inherit;
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
[picker-footer] button:disabled {
|
|
250
|
+
cursor: default;
|
|
251
|
+
opacity: 0.45;
|
|
252
|
+
}
|
|
253
|
+
|
|
211
254
|
/* 설명은 **본문**이다 — tooltip 이 아니다. 체크박스 아래 칸을 비워 두고 거기 놓는다. */
|
|
212
255
|
ul[axes] [says] {
|
|
213
256
|
grid-column: 2;
|
|
@@ -257,6 +300,10 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
257
300
|
they cannot answer "what does this role do", because that reading needs 46 clicks.
|
|
258
301
|
*/
|
|
259
302
|
[granted] {
|
|
303
|
+
flex: 1;
|
|
304
|
+
min-height: 0;
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-direction: column;
|
|
260
307
|
margin: var(--spacing-medium, 8px) var(--spacing-large, 12px) 0;
|
|
261
308
|
border: 1px solid var(--md-sys-color-outline-variant);
|
|
262
309
|
border-radius: var(--md-sys-shape-corner-medium, 10px);
|
|
@@ -276,43 +323,21 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
276
323
|
color: var(--md-sys-color-outline);
|
|
277
324
|
}
|
|
278
325
|
|
|
326
|
+
/*
|
|
327
|
+
⚠ 여기 display: flex 와 flex-wrap: wrap 이 있었다 — 칩으로 그리던 시절의 규칙이다.
|
|
328
|
+
마크업을 줄 목록으로 바꾼 뒤에도 남아서 **문장이 한 줄씩 서지 않고 칩처럼 옆으로
|
|
329
|
+
흘렀다.** 내 ul[lines] 는 display 를 선언하지 않으므로 맞설 것이 없었다.
|
|
330
|
+
마크업을 갈 때는 그 마크업만 쓰던 CSS 를 함께 지운다.
|
|
331
|
+
|
|
332
|
+
높이도 바뀐다. max-height: 22vh 는 격자 두 판이 늘 아래에 펼쳐져 있던 시절의 값이다.
|
|
333
|
+
지금은 **이 목록이 본문**이므로(§2) 남는 높이를 여기가 받고, 59줄은 여기서 흐른다.
|
|
334
|
+
*/
|
|
279
335
|
[granted] ul {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
gap: 4px 6px;
|
|
283
|
-
list-style: none;
|
|
284
|
-
margin: 0;
|
|
285
|
-
padding: 0 12px 10px;
|
|
286
|
-
/* Long-running roles reach every module; cap the block so the panels stay on screen. */
|
|
287
|
-
max-height: 22vh;
|
|
336
|
+
flex: 1;
|
|
337
|
+
min-height: 0;
|
|
288
338
|
overflow: auto;
|
|
289
339
|
}
|
|
290
340
|
|
|
291
|
-
[granted] li {
|
|
292
|
-
display: inline-flex;
|
|
293
|
-
align-items: baseline;
|
|
294
|
-
gap: 5px;
|
|
295
|
-
padding: 3px 9px;
|
|
296
|
-
border-radius: var(--md-sys-shape-corner-small, 6px);
|
|
297
|
-
background-color: var(--md-sys-color-surface-variant);
|
|
298
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
299
|
-
font-size: 0.82rem;
|
|
300
|
-
cursor: pointer;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
[granted] li:hover {
|
|
304
|
-
background-color: var(--md-sys-color-secondary-container);
|
|
305
|
-
color: var(--md-sys-color-on-secondary-container);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
[granted] li [axes] {
|
|
309
|
-
color: var(--md-sys-color-primary);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
[granted] li:hover [axes] {
|
|
313
|
-
color: var(--md-sys-color-on-secondary-container);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
341
|
[granted] [empty] {
|
|
317
342
|
padding: 0 12px 10px;
|
|
318
343
|
}
|
|
@@ -332,6 +357,101 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
332
357
|
color: var(--md-sys-color-on-error-container);
|
|
333
358
|
}
|
|
334
359
|
|
|
360
|
+
/*
|
|
361
|
+
── 할 수 있는 일 (본문) ────────────────────────────────────────────────
|
|
362
|
+
읽는 것이 먼저이므로 글자를 본문 크기로 둔다. 격자의 0.85rem 보다 크다.
|
|
363
|
+
*/
|
|
364
|
+
ul[lines] {
|
|
365
|
+
list-style: none;
|
|
366
|
+
margin: 0;
|
|
367
|
+
padding: 0 12px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
ul[lines] li {
|
|
371
|
+
display: flex;
|
|
372
|
+
align-items: center;
|
|
373
|
+
gap: 10px;
|
|
374
|
+
padding: 9px 2px;
|
|
375
|
+
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
ul[lines] li:last-child {
|
|
379
|
+
border-bottom: none;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
ul[lines] [line] {
|
|
383
|
+
flex: 1;
|
|
384
|
+
font-size: 0.95rem;
|
|
385
|
+
line-height: 1.5;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* 문장이 없어서 이름으로 지은 줄 — 문장과 구별되게 조용히 둔다. */
|
|
389
|
+
ul[lines] [named] {
|
|
390
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
ul[lines] li[ghost] [line] {
|
|
394
|
+
text-decoration: line-through;
|
|
395
|
+
color: var(--md-sys-color-outline);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
ul[lines] [tag] {
|
|
399
|
+
margin-left: 8px;
|
|
400
|
+
font-size: 0.78rem;
|
|
401
|
+
text-decoration: none;
|
|
402
|
+
color: var(--md-sys-color-outline);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* 떼기 — 줄마다 있으므로 조용하고, 그 줄에 머물 때 드러난다. */
|
|
406
|
+
button[drop] {
|
|
407
|
+
flex: none;
|
|
408
|
+
display: inline-flex;
|
|
409
|
+
padding: 3px;
|
|
410
|
+
color: var(--md-sys-color-outline);
|
|
411
|
+
background-color: transparent;
|
|
412
|
+
border: none;
|
|
413
|
+
border-radius: var(--md-sys-shape-corner-full, 999px);
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
}
|
|
416
|
+
button[drop] md-icon {
|
|
417
|
+
--md-icon-size: 18px;
|
|
418
|
+
}
|
|
419
|
+
ul[lines] li:hover button[drop] {
|
|
420
|
+
color: var(--md-sys-color-error);
|
|
421
|
+
background-color: var(--md-sys-color-error-container);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
button[add] {
|
|
425
|
+
display: inline-flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 4px;
|
|
428
|
+
margin: 10px 12px 12px;
|
|
429
|
+
padding: 8px 14px 8px 10px;
|
|
430
|
+
font: var(--label-font, inherit);
|
|
431
|
+
font-size: 0.85rem;
|
|
432
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
433
|
+
background-color: var(--md-sys-color-secondary-container);
|
|
434
|
+
border: none;
|
|
435
|
+
border-radius: var(--md-sys-shape-corner-full, 999px);
|
|
436
|
+
cursor: pointer;
|
|
437
|
+
}
|
|
438
|
+
button[add] md-icon {
|
|
439
|
+
--md-icon-size: 18px;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
button[close] {
|
|
443
|
+
margin-left: auto;
|
|
444
|
+
display: inline-flex;
|
|
445
|
+
padding: 2px;
|
|
446
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
447
|
+
background-color: transparent;
|
|
448
|
+
border: none;
|
|
449
|
+
cursor: pointer;
|
|
450
|
+
}
|
|
451
|
+
button[close] md-icon {
|
|
452
|
+
--md-icon-size: 18px;
|
|
453
|
+
}
|
|
454
|
+
|
|
335
455
|
[empty] {
|
|
336
456
|
padding: var(--spacing-large, 12px);
|
|
337
457
|
color: var(--md-sys-color-on-surface-variant);
|
|
@@ -352,18 +472,59 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
352
472
|
return this.fetchState === 'loaded' && !this.saving && !this.inherited;
|
|
353
473
|
}
|
|
354
474
|
get visibleModules() {
|
|
355
|
-
|
|
356
|
-
return keyword ? this.modules.filter(m => m.category.toLowerCase().includes(keyword)) : this.modules;
|
|
475
|
+
return filterPrivilegeModules(this.modules, this.keyword, moduleLabel);
|
|
357
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* `role-privilege-picker` 는 이 페이지의 상태를 읽어 그린다. 둘은 서로 다른 shadow root 이므로
|
|
479
|
+
* 여기 state 만 바꾸면 picker 는 다시 그릴 이유를 알지 못한다. 검색·모듈 선택은 그 누락 때문에
|
|
480
|
+
* 눌러도 화면이 그대로였다.
|
|
481
|
+
*/
|
|
482
|
+
refreshPicker() {
|
|
483
|
+
this.picker?.requestUpdate();
|
|
484
|
+
}
|
|
485
|
+
selectModule(category) {
|
|
486
|
+
if (this.selected === category) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this.selected = category;
|
|
490
|
+
this.refreshPicker();
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* ox-input-search 의 `change` 는 입력을 끝내고 포커스를 옮길 때만 온다. 모듈을 찾는 칸은
|
|
494
|
+
* 입력하는 즉시 좁혀져야 하므로 shadow 경계를 넘어오는 native input 이벤트도 함께 받는다.
|
|
495
|
+
*/
|
|
496
|
+
updateKeyword(event) {
|
|
497
|
+
const nativeInput = event.composedPath().find(node => node instanceof HTMLInputElement);
|
|
498
|
+
const keyword = nativeInput?.value ?? event.target?.value ?? '';
|
|
499
|
+
this.keyword = keyword;
|
|
500
|
+
/* 검색 결과에 현재 선택 모듈이 없으면 첫 결과를 바로 연다. 오른쪽에 이전 모듈의 축이
|
|
501
|
+
남아 있으면 검색이 안 된 것처럼 보이기 때문이다. */
|
|
502
|
+
this.selected = selectedVisibleModule(this.visibleModules, this.selected);
|
|
503
|
+
this.refreshPicker();
|
|
504
|
+
}
|
|
505
|
+
/*
|
|
506
|
+
* The page is named after the screen, not after the role it happens to be showing.
|
|
507
|
+
*
|
|
508
|
+
* It used to carry `this.subject.name`, and the reader then saw "Administrator" three times
|
|
509
|
+
* on one screen: the tab, the page header, and the subject block in the body. Worse, the page
|
|
510
|
+
* had no name of its own - nothing said what this screen is for.
|
|
511
|
+
*
|
|
512
|
+
* A record-valued title does have precedent here (appliance, app-binding, application in this
|
|
513
|
+
* package, and board-ui's play list - 4 of the 6 object-form titles), but those pages are the
|
|
514
|
+
* record's own detail. This one is one facet of a role, and the role is already named in the
|
|
515
|
+
* body, where a description can sit beside it.
|
|
516
|
+
*/
|
|
358
517
|
get context() {
|
|
359
518
|
return {
|
|
360
|
-
title:
|
|
519
|
+
title: { icon: 'vpn_key', text: i18next.t('title.role privileges') },
|
|
361
520
|
help: 'auth/roles',
|
|
362
521
|
actions: this.inherited
|
|
363
522
|
? []
|
|
364
523
|
: [
|
|
365
524
|
{
|
|
366
525
|
title: i18next.t('button.save'),
|
|
526
|
+
disabled: !this.dirty || !this.savable,
|
|
527
|
+
disabledTitle: this.saving ? '권한을 저장하고 있습니다.' : '권한을 바꾼 뒤 저장할 수 있습니다.',
|
|
367
528
|
action: this.onSave.bind(this)
|
|
368
529
|
}
|
|
369
530
|
]
|
|
@@ -385,41 +546,16 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
385
546
|
})}
|
|
386
547
|
</div>`
|
|
387
548
|
: nothing}
|
|
388
|
-
${this.renderSubject()} ${this.
|
|
389
|
-
|
|
390
|
-
<div body>
|
|
391
|
-
<div panel>
|
|
392
|
-
<header>
|
|
393
|
-
${i18next.t('field.module')}
|
|
394
|
-
<span count>${this.modules.length}</span>
|
|
395
|
-
</header>
|
|
396
|
-
<md-filled-text-field
|
|
397
|
-
type="text"
|
|
398
|
-
label=${i18next.t('text.filter by module name')}
|
|
399
|
-
.value=${this.keyword}
|
|
400
|
-
@input=${(e) => (this.keyword = e.target.value)}
|
|
401
|
-
></md-filled-text-field>
|
|
402
|
-
<div scroll>${this.renderModules()}</div>
|
|
403
|
-
</div>
|
|
404
|
-
|
|
405
|
-
<div panel>
|
|
406
|
-
<header>
|
|
407
|
-
${this.selected ? moduleLabel(this.selected) : i18next.t('field.privileges')}
|
|
408
|
-
<span count>${i18next.t('text.x of y on', { x: this.assigned.size, y: this.cellCount() })}</span>
|
|
409
|
-
</header>
|
|
410
|
-
<div scroll>${this.renderAxes()}</div>
|
|
411
|
-
</div>
|
|
412
|
-
</div>
|
|
549
|
+
${this.renderSubject()} ${this.renderHolders()} ${this.renderGranted()}
|
|
413
550
|
`;
|
|
414
551
|
}
|
|
415
552
|
/**
|
|
416
|
-
*
|
|
553
|
+
* 어느 역할을 고치고 있나, 그리고 그것이 무엇을 위한 것인가.
|
|
417
554
|
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* no way to tell "Administrator" from "PLANT-MANAGER" beyond the word itself.
|
|
555
|
+
* 머리말은 이름만 들고 있었고, **직무를 말하는 한 줄인 설명은 목록에만 있었다.** 링크로
|
|
556
|
+
* 들어온 사람은 `Administrator` 와 `PLANT-MANAGER` 를 낱말 말고는 구별할 방법이 없다.
|
|
421
557
|
*
|
|
422
|
-
*
|
|
558
|
+
* 설명이 없는 역할은 없다고 말한다. 다음에 할 일은 그것을 쓰는 것이고, 자리는 목록 격자다.
|
|
423
559
|
*/
|
|
424
560
|
renderSubject() {
|
|
425
561
|
if (this.fetchState !== 'loaded') {
|
|
@@ -435,44 +571,165 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
435
571
|
`;
|
|
436
572
|
}
|
|
437
573
|
/**
|
|
438
|
-
*
|
|
574
|
+
* **이 역할이 할 수 있는 일 — 이것이 본문이다.**
|
|
575
|
+
*
|
|
576
|
+
* ── 뒤집은 이유 (UX 레인 §2, 사용자 평가) ──────────────────────────────────
|
|
577
|
+
* 격자는 「이 모듈에서 무엇이 켜졌나」에 답한다. 관리자가 묻는 것은 **「이 역할이 무엇을
|
|
578
|
+
* 하나」**이고, 격자로 그것을 읽으려면 모듈 46개를 한 번씩 열어야 한다. 역할을 부여하는
|
|
579
|
+
* 사람은 부여하기 **전에** 그 답이 필요하다.
|
|
580
|
+
*
|
|
581
|
+
* 그래서 켜진 것을 문장으로 늘어놓는 것이 본문이 되고, 격자는 **더할 때 쓰는 도구**로
|
|
582
|
+
* 자리를 옮긴다(`renderAdd`). 문장이 줄마다 있으므로 배우는 일이 따로 없다.
|
|
439
583
|
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* granting a role needs the whole answer before they grant it.
|
|
584
|
+
* ── 켜진 것만 낸다 ─────────────────────────────────────────────────────────
|
|
585
|
+
* 전체를 여기 또 그리면 같은 46 모듈이 화면에 두 번 선다. 전체는 「더하기」 판이 갖는다.
|
|
443
586
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
587
|
+
* ── 글이 없을 때 ───────────────────────────────────────────────────────────
|
|
588
|
+
* 「더하기」 판에서는 문장이 없으면 **없다고 말해도 된다** — 체크박스 옆에 「읽기」라는
|
|
589
|
+
* 라벨이 이미 있어서 무엇을 켜는지 알 수 있다.
|
|
446
590
|
*
|
|
447
|
-
*
|
|
591
|
+
* 여기서는 다르다. **이 줄이 곧 그 권한의 신분이다.** 없다고만 쓰면 무엇을 떼는지 모르는
|
|
592
|
+
* 줄이 되므로, 그때는 `모듈 · 축` 으로 이름을 짓는다. 그것은 코드 낱말이 아니다 —
|
|
593
|
+
* `privilege.category.*` 57개와 「읽기/쓰기」가 사람 말이다.
|
|
448
594
|
*/
|
|
449
595
|
renderGranted() {
|
|
450
|
-
if (this.fetchState !== 'loaded'
|
|
596
|
+
if (this.fetchState !== 'loaded') {
|
|
451
597
|
return nothing;
|
|
452
598
|
}
|
|
453
|
-
const
|
|
454
|
-
.
|
|
455
|
-
|
|
456
|
-
axes: module.cells.filter(cell => this.assigned.has(cell.id))
|
|
457
|
-
}))
|
|
458
|
-
.filter(entry => entry.axes.length);
|
|
599
|
+
const granted = this.modules
|
|
600
|
+
.flatMap(module => module.cells.map(cell => ({ category: module.category, cell })))
|
|
601
|
+
.filter(entry => this.assigned.has(entry.cell.id));
|
|
459
602
|
return html `
|
|
460
603
|
<div granted>
|
|
461
604
|
<header>
|
|
462
605
|
${i18next.t('field.granted privileges')}
|
|
463
|
-
<span count>${
|
|
606
|
+
<span count>${granted.length}</span>
|
|
464
607
|
</header>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
@click=${() => (this.selected = entry.category)}
|
|
470
|
-
>
|
|
471
|
-
${moduleLabel(entry.category)}
|
|
472
|
-
<span axes>${entry.axes.map(cell => axisLabel(cell.axis)).join(' · ')}</span>
|
|
473
|
-
</li>`)}
|
|
608
|
+
|
|
609
|
+
${granted.length
|
|
610
|
+
? html `<ul lines>
|
|
611
|
+
${granted.map(({ category, cell }) => this.renderGrantedLine(category, cell))}
|
|
474
612
|
</ul>`
|
|
475
613
|
: html `<div empty>${i18next.t('text.nothing is on for this role yet')}</div>`}
|
|
614
|
+
${this.renderAdd()}
|
|
615
|
+
</div>
|
|
616
|
+
`;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* 한 줄 — 문장과 [떼기].
|
|
620
|
+
*
|
|
621
|
+
* 유령(이 배포의 어떤 코드도 요구하지 않는 권한)은 취소선으로 보이고 **떼는 것만** 된다.
|
|
622
|
+
* 이미 붙어 있으니 뗄 수 있어야 하고, 붙여도 아무 일이 없으니 다시 켤 이유가 없다.
|
|
623
|
+
*/
|
|
624
|
+
renderGrantedLine(category, cell) {
|
|
625
|
+
const written = axisText(category, cell);
|
|
626
|
+
return html `
|
|
627
|
+
<li ?ghost=${cell.deprecated}>
|
|
628
|
+
<div line>
|
|
629
|
+
${written || html `<span named>${moduleLabel(category)} · ${axisLabel(cell.axis)}</span>`}
|
|
630
|
+
${cell.deprecated ? html `<span tag>${i18next.t('text.privilege no longer declared')}</span>` : nothing}
|
|
631
|
+
</div>
|
|
632
|
+
|
|
633
|
+
${this.inherited
|
|
634
|
+
? nothing
|
|
635
|
+
: html `<button drop title=${i18next.t('button.drop')} @click=${() => this.onToggle(cell.id, false)}>
|
|
636
|
+
<md-icon>close</md-icon>
|
|
637
|
+
</button>`}
|
|
638
|
+
</li>
|
|
639
|
+
`;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* 「+ 할 수 있는 일 더하기」 — 격자가 여기 들어간다.
|
|
643
|
+
*
|
|
644
|
+
* 열어 두지 않는 이유는 하나다 — **처음 읽는 것이 「이 역할이 무엇을 하나」여야 한다.**
|
|
645
|
+
* 46 모듈이 늘 펼쳐져 있으면 그 답이 화면 아래로 밀린다.
|
|
646
|
+
*
|
|
647
|
+
* 상속 역할에는 내주지 않는다. 여기서 저장할 수 없으므로 켜 보게 하는 것이 헛수고다 —
|
|
648
|
+
* 대신 위쪽 안내가 어느 도메인에서 고치는지 말한다.
|
|
649
|
+
*/
|
|
650
|
+
renderAdd() {
|
|
651
|
+
if (this.inherited || this.fetchState !== 'loaded') {
|
|
652
|
+
return nothing;
|
|
653
|
+
}
|
|
654
|
+
if (!this.modules.length) {
|
|
655
|
+
return html `<div note attention>${i18next.t('text.no privilege declared in this deployment')}</div>`;
|
|
656
|
+
}
|
|
657
|
+
return html `
|
|
658
|
+
<button add @click=${() => this.openAdd()}>
|
|
659
|
+
<md-icon>add</md-icon>${i18next.t('button.add what it can do')}
|
|
660
|
+
</button>
|
|
661
|
+
`;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* 고르는 판을 **넓은 팝업**으로 연다.
|
|
665
|
+
*
|
|
666
|
+
* 사용자 말씀이다 — 「'할 수 있는 일 더하기' 작동을 팝업으로 넓게 할 수 있었으면 어떤가?
|
|
667
|
+
* 지금은 좁아보이고」. 페이지 안에 펼치면 본문(할 수 있는 일)이 아래로 밀리고, 남은 폭에서
|
|
668
|
+
* 모듈 46개와 축 목록을 함께 봐야 한다. 고르는 동안은 **고르는 것만** 보는 것이 맞다.
|
|
669
|
+
*
|
|
670
|
+
* ```
|
|
671
|
+
* [읽었음] ox-floating-overlay.ts:126-134 [size=large] width 100% · height 100%
|
|
672
|
+
* ```
|
|
673
|
+
*
|
|
674
|
+
* `escapable` 을 건드리지 않는다 — 켠 것은 팝업이 아니라 **이 페이지의 `assigned`** 에 바로
|
|
675
|
+
* 들어가므로 닫든 ESC 를 누르든 잃는 것이 없다. 물어볼 것이 없는 판에 물으면 그 물음이 값을
|
|
676
|
+
* 잃는다. 저장은 여전히 툴바가 한다.
|
|
677
|
+
*
|
|
678
|
+
* ⚠ 부품을 **동적으로 들인다.** 그 부품은 스타일 한 벌을 이 클래스에서 가져가므로, 위에서
|
|
679
|
+
* 정적으로 import 하면 순환이 된다. 여는 순간에 들이면 그 고리가 끊긴다.
|
|
680
|
+
*/
|
|
681
|
+
async openAdd() {
|
|
682
|
+
await import('../../components/role-privilege-picker.js');
|
|
683
|
+
const popup = openPopup(html `<role-privilege-picker .page=${this} @close=${() => popup.close()}></role-privilege-picker>`, {
|
|
684
|
+
backdrop: true,
|
|
685
|
+
size: 'large',
|
|
686
|
+
title: i18next.t('button.add what it can do')
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* 고르는 판의 몸체 — **팝업이 이것을 그린다.**
|
|
691
|
+
*
|
|
692
|
+
* 판을 부품 안으로 옮기지 않는다. 모듈 접기·축 줄·유령 규칙이 전부 이 페이지의 상태
|
|
693
|
+
* (`modules`·`assigned`·`selected`·`keyword`)를 보기 때문이고, 옮기면 **상태가 두 벌**이 된다.
|
|
694
|
+
* 두 벌이 되는 순간 어느 쪽이 저장 범위인지 흐려진다 — 옛 편집기가 `querySelectorAll` 로
|
|
695
|
+
* 범위를 만들던 결함이 정확히 그 모양이었다.
|
|
696
|
+
*/
|
|
697
|
+
renderPickerBody() {
|
|
698
|
+
return html `
|
|
699
|
+
<div body>
|
|
700
|
+
<div panel>
|
|
701
|
+
<header>
|
|
702
|
+
${i18next.t('field.module')}
|
|
703
|
+
<span count>${this.modules.length}</span>
|
|
704
|
+
</header>
|
|
705
|
+
<ox-input-search
|
|
706
|
+
.placeholder=${i18next.t('text.filter by module name')}
|
|
707
|
+
.value=${this.keyword}
|
|
708
|
+
@input=${(event) => this.updateKeyword(event)}
|
|
709
|
+
@change=${(event) => this.updateKeyword(event)}
|
|
710
|
+
></ox-input-search>
|
|
711
|
+
<div scroll>${this.renderModules()}</div>
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<div panel>
|
|
715
|
+
<header>
|
|
716
|
+
${this.selected ? moduleLabel(this.selected) : i18next.t('field.privileges')}
|
|
717
|
+
<span count>${i18next.t('text.x of y on', { x: this.assigned.size, y: this.cellCount() })}</span>
|
|
718
|
+
</header>
|
|
719
|
+
<div scroll>${this.renderAxes()}</div>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
<div picker-footer>
|
|
723
|
+
<span>
|
|
724
|
+
${this.saving
|
|
725
|
+
? '권한을 저장하고 있습니다.'
|
|
726
|
+
: this.dirty
|
|
727
|
+
? '변경 사항이 있습니다. 저장해야 적용됩니다.'
|
|
728
|
+
: '권한을 바꾼 뒤 저장할 수 있습니다.'}
|
|
729
|
+
</span>
|
|
730
|
+
<button ?disabled=${!this.savable || !this.dirty} @click=${() => this.onSave()}>
|
|
731
|
+
${this.saving ? '저장 중…' : i18next.t('button.save')}
|
|
732
|
+
</button>
|
|
476
733
|
</div>
|
|
477
734
|
`;
|
|
478
735
|
}
|
|
@@ -508,7 +765,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
508
765
|
${this.visibleModules.map(module => {
|
|
509
766
|
const on = module.cells.filter(cell => this.assigned.has(cell.id)).length;
|
|
510
767
|
return html `
|
|
511
|
-
<li ?selected=${module.category === this.selected} @click=${() =>
|
|
768
|
+
<li ?selected=${module.category === this.selected} @click=${() => this.selectModule(module.category)}>
|
|
512
769
|
<span>${moduleLabel(module.category)}</span>
|
|
513
770
|
${on
|
|
514
771
|
? html `<span on>${i18next.t('text.x on', { x: on })}</span>`
|
|
@@ -533,27 +790,19 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
533
790
|
${module.cells.map(cell => {
|
|
534
791
|
const on = this.assigned.has(cell.id);
|
|
535
792
|
return html `
|
|
536
|
-
<li
|
|
537
|
-
|
|
793
|
+
<li
|
|
794
|
+
?ghost=${cell.deprecated}
|
|
795
|
+
?readonly=${this.inherited || (cell.deprecated && !on)}
|
|
796
|
+
@click=${(event) => this.toggleCellFromRow(event, cell, on)}
|
|
797
|
+
>
|
|
798
|
+
<input
|
|
799
|
+
type="checkbox"
|
|
538
800
|
id="cell-${cell.id}"
|
|
539
|
-
touch-target="wrapper"
|
|
540
801
|
.checked=${on}
|
|
541
802
|
?disabled=${this.inherited || (cell.deprecated && !on)}
|
|
542
803
|
@change=${(e) => this.onToggle(cell.id, e.target.checked)}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
md-checkbox 는 native input 이 아니라서 label for 로는 안 눌린다.
|
|
546
|
-
글자를 눌러도 켜지는 것이 사람이 기대하는 것이므로 같은 처리기를 붙인다.
|
|
547
|
-
-->
|
|
548
|
-
<label
|
|
549
|
-
for="cell-${cell.id}"
|
|
550
|
-
@click=${() => {
|
|
551
|
-
if (!this.inherited && !(cell.deprecated && !on)) {
|
|
552
|
-
this.onToggle(cell.id, !on);
|
|
553
|
-
}
|
|
554
|
-
}}
|
|
555
|
-
>${axisLabel(cell.axis)}</label
|
|
556
|
-
>
|
|
804
|
+
/>
|
|
805
|
+
<label for="cell-${cell.id}">${axisLabel(cell.axis)}</label>
|
|
557
806
|
|
|
558
807
|
${cell.deprecated
|
|
559
808
|
? html `<div says>${i18next.t('text.privilege no longer declared')}</div>`
|
|
@@ -658,15 +907,43 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
658
907
|
}
|
|
659
908
|
this.modules = [...byCategory.values()].sort((a, b) => moduleLabel(a.category).localeCompare(moduleLabel(b.category)));
|
|
660
909
|
this.assigned = assigned;
|
|
910
|
+
this.dirty = false;
|
|
661
911
|
/* 고른 것이 없으면 첫 모듈을 연다 — 빈 오른쪽 판을 처음 화면으로 두지 않는다. */
|
|
662
912
|
if (!this.modules.find(m => m.category === this.selected)) {
|
|
663
913
|
this.selected = this.modules[0]?.category || '';
|
|
664
914
|
}
|
|
665
915
|
}
|
|
666
916
|
onToggle(id, checked) {
|
|
917
|
+
if (this.assigned.has(id) === checked) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
667
920
|
const next = new Set(this.assigned);
|
|
668
921
|
checked ? next.add(id) : next.delete(id);
|
|
669
922
|
this.assigned = next;
|
|
923
|
+
this.dirty = true;
|
|
924
|
+
this.updateContext();
|
|
925
|
+
/* 팝업은 자기 상태를 안 들고 이 페이지를 본다 — 열려 있으면 다시 그리라고 알린다. */
|
|
926
|
+
this.refreshPicker();
|
|
927
|
+
}
|
|
928
|
+
/** 라벨·설명을 포함한 행 전체를 누를 수 있게 한다. checkbox와 키보드 Space는 native change가 맡는다. */
|
|
929
|
+
toggleCellFromRow(event, cell, on) {
|
|
930
|
+
if (this.inherited || (cell.deprecated && !on) || event.target instanceof HTMLInputElement) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
/* label의 기본 동작이 checkbox change를 한 번 더 보내지 않게 막는다. */
|
|
934
|
+
event.preventDefault();
|
|
935
|
+
this.onToggle(cell.id, !on);
|
|
936
|
+
}
|
|
937
|
+
async canDeactivate() {
|
|
938
|
+
if (!this.dirty) {
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
return await OxPrompt.open({
|
|
942
|
+
title: '저장하지 않은 변경 사항',
|
|
943
|
+
text: '수정된 권한이 있습니다. 저장하지 않고 이 페이지를 나가시겠습니까?',
|
|
944
|
+
confirmButton: { text: '저장하지 않고 나가기' },
|
|
945
|
+
cancelButton: { text: i18next.t('button.cancel') }
|
|
946
|
+
});
|
|
670
947
|
}
|
|
671
948
|
/**
|
|
672
949
|
* 저장.
|
|
@@ -681,6 +958,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
681
958
|
return;
|
|
682
959
|
}
|
|
683
960
|
this.saving = true;
|
|
961
|
+
this.updateContext();
|
|
684
962
|
try {
|
|
685
963
|
const response = await client.mutate({
|
|
686
964
|
mutation: gql `
|
|
@@ -703,9 +981,11 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
703
981
|
document.dispatchEvent(new CustomEvent('notify', { detail: { message: i18next.t('text.data_updated_successfully') } }));
|
|
704
982
|
/* 유령을 뗐으면 목록에서 사라져야 하고, 켠 것이 요약에 반영돼야 한다. */
|
|
705
983
|
await this.fetch();
|
|
984
|
+
this.dirty = false;
|
|
706
985
|
}
|
|
707
986
|
finally {
|
|
708
987
|
this.saving = false;
|
|
988
|
+
this.updateContext();
|
|
709
989
|
}
|
|
710
990
|
}
|
|
711
991
|
};
|
|
@@ -749,6 +1029,10 @@ __decorate([
|
|
|
749
1029
|
state(),
|
|
750
1030
|
__metadata("design:type", Boolean)
|
|
751
1031
|
], RolePrivilegePage.prototype, "saving", void 0);
|
|
1032
|
+
__decorate([
|
|
1033
|
+
state(),
|
|
1034
|
+
__metadata("design:type", Boolean)
|
|
1035
|
+
], RolePrivilegePage.prototype, "dirty", void 0);
|
|
752
1036
|
RolePrivilegePage = __decorate([
|
|
753
1037
|
customElement('role-privilege-page')
|
|
754
1038
|
], RolePrivilegePage);
|
|
@@ -787,11 +1071,14 @@ function axisLabel(axis) {
|
|
|
787
1071
|
* 그대로** 돌려주므로 화면에 `privilege.allows.board.query` 가 찍힌다. 빈 문자열을 기본값으로
|
|
788
1072
|
* 줘서 「없음」이 거짓으로 판정되게 한다.
|
|
789
1073
|
*/
|
|
790
|
-
function
|
|
1074
|
+
function axisText(category, cell) {
|
|
791
1075
|
if (cell.description) {
|
|
792
|
-
return
|
|
1076
|
+
return cell.description;
|
|
793
1077
|
}
|
|
794
|
-
|
|
1078
|
+
return i18next.t(`privilege.allows.${category}.${cell.axis}`, { defaultValue: '' });
|
|
1079
|
+
}
|
|
1080
|
+
function axisSays(category, cell) {
|
|
1081
|
+
const written = axisText(category, cell);
|
|
795
1082
|
return written ? html `<div says>${written}</div>` : null;
|
|
796
1083
|
}
|
|
797
1084
|
function axisOrder(axis) {
|