@things-factory/auth-ui 10.1.7 → 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.d.ts +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 +3 -0
- package/dist-client/components/role-privilege-picker.js +19 -0
- package/dist-client/components/role-privilege-picker.js.map +1 -1
- package/dist-client/pages/role/role-privilege-page.d.ts +18 -0
- package/dist-client/pages/role/role-privilege-page.js +123 -15
- package/dist-client/pages/role/role-privilege-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 검색·선택 상태는 Lit 화면 밖에서도 같은 규칙으로 검증한다. */
|
|
2
|
+
export type PrivilegeModuleRef = {
|
|
3
|
+
category: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* 모듈은 코드(category)와 사람이 보는 이름 모두로 찾는다. 관리자가 「사용자」를 입력했는데
|
|
7
|
+
* 내부 코드 `user`만 비교하면 결과가 없다고 나오는 것은 검색으로서 실패다.
|
|
8
|
+
*/
|
|
9
|
+
export declare function filterPrivilegeModules<T extends PrivilegeModuleRef>(modules: T[], keyword: string, labelOf: (category: string) => string): T[];
|
|
10
|
+
/** 검색 결과가 바뀌어 선택한 모듈이 사라지면 첫 결과를 연다. */
|
|
11
|
+
export declare function selectedVisibleModule<T extends PrivilegeModuleRef>(modules: T[], selected: string): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 모듈은 코드(category)와 사람이 보는 이름 모두로 찾는다. 관리자가 「사용자」를 입력했는데
|
|
3
|
+
* 내부 코드 `user`만 비교하면 결과가 없다고 나오는 것은 검색으로서 실패다.
|
|
4
|
+
*/
|
|
5
|
+
export function filterPrivilegeModules(modules, keyword, labelOf) {
|
|
6
|
+
const needle = keyword.trim().toLocaleLowerCase();
|
|
7
|
+
if (!needle)
|
|
8
|
+
return modules;
|
|
9
|
+
return modules.filter(module => [module.category, labelOf(module.category)].some(value => value.toLocaleLowerCase().includes(needle)));
|
|
10
|
+
}
|
|
11
|
+
/** 검색 결과가 바뀌어 선택한 모듈이 사라지면 첫 결과를 연다. */
|
|
12
|
+
export function selectedVisibleModule(modules, selected) {
|
|
13
|
+
return modules.some(module => module.category === selected) ? selected : modules[0]?.category || '';
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=role-privilege-picker-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-privilege-picker-state.js","sourceRoot":"","sources":["../../client/components/role-privilege-picker-state.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAAY,EACZ,OAAe,EACf,OAAqC;IAErC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,iBAAiB,EAAE,CAAA;IACjD,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAA;IAE3B,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAC7B,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CACtG,CAAA;AACH,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,qBAAqB,CAA+B,OAAY,EAAE,QAAgB;IAChG,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,IAAI,EAAE,CAAA;AACrG,CAAC","sourcesContent":["/** 검색·선택 상태는 Lit 화면 밖에서도 같은 규칙으로 검증한다. */\nexport type PrivilegeModuleRef = { category: string }\n\n/**\n * 모듈은 코드(category)와 사람이 보는 이름 모두로 찾는다. 관리자가 「사용자」를 입력했는데\n * 내부 코드 `user`만 비교하면 결과가 없다고 나오는 것은 검색으로서 실패다.\n */\nexport function filterPrivilegeModules<T extends PrivilegeModuleRef>(\n modules: T[],\n keyword: string,\n labelOf: (category: string) => string\n): T[] {\n const needle = keyword.trim().toLocaleLowerCase()\n if (!needle) return modules\n\n return modules.filter(module =>\n [module.category, labelOf(module.category)].some(value => value.toLocaleLowerCase().includes(needle))\n )\n}\n\n/** 검색 결과가 바뀌어 선택한 모듈이 사라지면 첫 결과를 연다. */\nexport function selectedVisibleModule<T extends PrivilegeModuleRef>(modules: T[], selected: string): string {\n return modules.some(module => module.category === selected) ? selected : modules[0]?.category || ''\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { filterPrivilegeModules, selectedVisibleModule } from './role-privilege-picker-state';
|
|
2
|
+
const modules = [{ category: 'user' }, { category: 'board' }, { category: 'worklist' }];
|
|
3
|
+
const labels = { user: '사용자', board: '보드', worklist: '작업지시' };
|
|
4
|
+
const labelOf = (category) => labels[category] || category;
|
|
5
|
+
describe('권한 모듈 검색', () => {
|
|
6
|
+
it('화면에 보이는 한글 모듈명으로 찾는다', () => {
|
|
7
|
+
expect(filterPrivilegeModules(modules, '사용자', labelOf)).toEqual([{ category: 'user' }]);
|
|
8
|
+
});
|
|
9
|
+
it('코드 모듈명으로도 찾는다', () => {
|
|
10
|
+
expect(filterPrivilegeModules(modules, 'work', labelOf)).toEqual([{ category: 'worklist' }]);
|
|
11
|
+
});
|
|
12
|
+
it('검색 결과에서 사라진 선택은 첫 결과로 옮긴다', () => {
|
|
13
|
+
const visible = filterPrivilegeModules(modules, '작업', labelOf);
|
|
14
|
+
expect(selectedVisibleModule(visible, 'user')).toBe('worklist');
|
|
15
|
+
});
|
|
16
|
+
it('아직 보이는 선택은 유지한다', () => {
|
|
17
|
+
expect(selectedVisibleModule(modules, 'board')).toBe('board');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
//# sourceMappingURL=role-privilege-picker-state.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-privilege-picker-state.test.js","sourceRoot":"","sources":["../../client/components/role-privilege-picker-state.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAA;AAE7F,MAAM,OAAO,GAAG,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAA;AACvF,MAAM,MAAM,GAA2B,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;AACrF,MAAM,OAAO,GAAG,CAAC,QAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAA;AAElE,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,sBAAsB,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;IACzF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,eAAe,EAAE,GAAG,EAAE;QACvB,MAAM,CAAC,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,CAAA;IAC9F,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,OAAO,GAAG,sBAAsB,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;QAC9D,MAAM,CAAC,qBAAqB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACjE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE;QACzB,MAAM,CAAC,qBAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC/D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { filterPrivilegeModules, selectedVisibleModule } from './role-privilege-picker-state'\n\nconst modules = [{ category: 'user' }, { category: 'board' }, { category: 'worklist' }]\nconst labels: Record<string, string> = { user: '사용자', board: '보드', worklist: '작업지시' }\nconst labelOf = (category: string) => labels[category] || category\n\ndescribe('권한 모듈 검색', () => {\n it('화면에 보이는 한글 모듈명으로 찾는다', () => {\n expect(filterPrivilegeModules(modules, '사용자', labelOf)).toEqual([{ category: 'user' }])\n })\n\n it('코드 모듈명으로도 찾는다', () => {\n expect(filterPrivilegeModules(modules, 'work', labelOf)).toEqual([{ category: 'worklist' }])\n })\n\n it('검색 결과에서 사라진 선택은 첫 결과로 옮긴다', () => {\n const visible = filterPrivilegeModules(modules, '작업', labelOf)\n expect(selectedVisibleModule(visible, 'user')).toBe('worklist')\n })\n\n it('아직 보이는 선택은 유지한다', () => {\n expect(selectedVisibleModule(modules, 'board')).toBe('board')\n })\n})\n"]}
|
|
@@ -31,7 +31,10 @@ export declare class RolePrivilegePicker extends LitElement {
|
|
|
31
31
|
static styles: CSSResultGroup;
|
|
32
32
|
/** 담는 페이지. 이 부품은 그것의 `renderPickerBody()` 를 그린다. */
|
|
33
33
|
page?: any;
|
|
34
|
+
private searchInput?;
|
|
35
|
+
private readonly onSearchInput;
|
|
34
36
|
connectedCallback(): void;
|
|
35
37
|
disconnectedCallback(): void;
|
|
36
38
|
render(): any;
|
|
39
|
+
updated(): void;
|
|
37
40
|
}
|
|
@@ -30,6 +30,14 @@ import { RolePrivilegePage } from '../pages/role/role-privilege-page.js';
|
|
|
30
30
|
* 페이지가 이 파일을 **동적으로** 들이기 때문에 이 import 가 순환이 되지 않는다.
|
|
31
31
|
*/
|
|
32
32
|
let RolePrivilegePicker = class RolePrivilegePicker extends LitElement {
|
|
33
|
+
constructor() {
|
|
34
|
+
super(...arguments);
|
|
35
|
+
this.onSearchInput = (event) => {
|
|
36
|
+
/* ox-input-search 는 값을 확정할 때만 host `change`를 낸다. 모듈을 찾는 일은 입력 중에도
|
|
37
|
+
결과가 바뀌어야 하므로, 이 팝업의 shadow root 안 native input을 직접 듣는다. */
|
|
38
|
+
this.page?.updateKeyword?.(event);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
33
41
|
static { this.styles = RolePrivilegePage.styles; }
|
|
34
42
|
connectedCallback() {
|
|
35
43
|
super.connectedCallback();
|
|
@@ -38,6 +46,8 @@ let RolePrivilegePicker = class RolePrivilegePicker extends LitElement {
|
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
disconnectedCallback() {
|
|
49
|
+
this.searchInput?.removeEventListener('input', this.onSearchInput);
|
|
50
|
+
this.searchInput = undefined;
|
|
41
51
|
super.disconnectedCallback();
|
|
42
52
|
if (this.page?.picker === this) {
|
|
43
53
|
this.page.picker = undefined;
|
|
@@ -46,6 +56,15 @@ let RolePrivilegePicker = class RolePrivilegePicker extends LitElement {
|
|
|
46
56
|
render() {
|
|
47
57
|
return this.page?.renderPickerBody?.() ?? html ``;
|
|
48
58
|
}
|
|
59
|
+
updated() {
|
|
60
|
+
const nextSearchInput = this.renderRoot.querySelector('ox-input-search')?.shadowRoot?.querySelector('input');
|
|
61
|
+
if (nextSearchInput === this.searchInput) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.searchInput?.removeEventListener('input', this.onSearchInput);
|
|
65
|
+
this.searchInput = nextSearchInput;
|
|
66
|
+
this.searchInput?.addEventListener('input', this.onSearchInput);
|
|
67
|
+
}
|
|
49
68
|
};
|
|
50
69
|
__decorate([
|
|
51
70
|
property({ type: Object }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-privilege-picker.js","sourceRoot":"","sources":["../../client/components/role-privilege-picker.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,oCAAoC,CAAA;AAC3C,OAAO,mCAAmC,CAAA;AAE1C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAEtC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AAExE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,UAAU;
|
|
1
|
+
{"version":3,"file":"role-privilege-picker.js","sourceRoot":"","sources":["../../client/components/role-privilege-picker.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,oCAAoC,CAAA;AAC3C,OAAO,mCAAmC,CAAA;AAE1C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAEtC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AAExE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,UAAU;IAA5C;;QAQY,kBAAa,GAAG,CAAC,KAAY,EAAE,EAAE;YAChD;yEAC6D;YAC7D,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC,KAAK,CAAC,CAAA;QACnC,CAAC,CAAA;IAmCH,CAAC;aA9CQ,WAAM,GAAmB,iBAAiB,CAAC,MAAwB,AAA7D,CAA6D;IAa1E,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QACzB,CAAC;IACH,CAAC;IAED,oBAAoB;QAClB,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,CAAC,CAAA;QAClE,IAAI,CAAC,WAAW,GAAG,SAAS,CAAA;QAC5B,KAAK,CAAC,oBAAoB,EAAE,CAAA;QAC5B,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QAC9B,CAAC;IACH,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,EAAE,IAAI,IAAI,CAAA,EAAE,CAAA;IAClD,CAAC;IAED,OAAO;QACL,MAAM,eAAe,GAAI,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,iBAAiB,CAAS,EAAE,UAAU,EAAE,aAAa,CAC1G,OAAO,CACwB,CAAA;QAEjC,IAAI,eAAe,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;YACzC,OAAM;QACR,CAAC;QAED,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,CAAC,CAAA;QAClE,IAAI,CAAC,WAAW,GAAG,eAAe,CAAA;QAClC,IAAI,CAAC,WAAW,EAAE,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,CAAC,CAAA;IACjE,CAAC;;AA1C2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;iDAAW;AAJ3B,mBAAmB;IAD/B,aAAa,CAAC,uBAAuB,CAAC;GAC1B,mBAAmB,CA+C/B","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/checkbox/checkbox.js'\nimport '@operato/input/ox-input-search.js'\n\nimport { html, LitElement } from 'lit'\nimport type { CSSResultGroup } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { RolePrivilegePage } from '../pages/role/role-privilege-page.js'\n\n/**\n * 「할 수 있는 일 더하기」의 **그릇**.\n *\n * ── 왜 팝업인가 (사용자, 2026-09-11) ────────────────────────────────────────\n * 「'할 수 있는 일 더하기' 작동을 팝업으로 넓게 할 수 있었으면 어떤가? 지금은 좁아보이고」\n *\n * 페이지 안에 펼치면 본문(할 수 있는 일)이 아래로 밀리고, 남은 폭에서 모듈 46개와 축 목록을\n * 함께 봐야 한다. 고르는 동안은 **고르는 것만** 보는 것이 맞다.\n *\n * ── 이 부품은 아무것도 안 그린다 ────────────────────────────────────────────\n * 모듈 접기·축 줄·유령 규칙이 전부 페이지의 상태(`modules`·`assigned`·`selected`·`keyword`)를\n * 본다. 그것을 여기로 옮기면 **상태가 두 벌**이 되고, 그 순간 어느 쪽이 저장 범위인지\n * 흐려진다 — 옛 편집기가 `querySelectorAll` 로 범위를 만들던 결함이 정확히 그 모양이었다.\n *\n * 그래서 그리기는 페이지가 하고(`renderPickerBody`), 여기는 담아서 띄우기만 한다. 대신\n * 페이지가 토글할 때 이 부품에게 다시 그리라고 알린다(`page.picker`).\n *\n * ── 스타일도 한 벌이다 ──────────────────────────────────────────────────────\n * 팝업은 페이지의 shadow root **밖**에 뜨므로 페이지 스타일이 여기까지 안 온다. 베껴 두면\n * 한쪽만 고쳐지는 날이 오고, 오늘 이 화면에서 그 부류를 이미 만났다 — 칩 시절 CSS 가 남아\n * 새 규칙을 이겼다. 그래서 페이지의 `styles` 를 그대로 가져간다.\n *\n * 페이지가 이 파일을 **동적으로** 들이기 때문에 이 import 가 순환이 되지 않는다.\n */\n@customElement('role-privilege-picker')\nexport class RolePrivilegePicker extends LitElement {\n static styles: CSSResultGroup = RolePrivilegePage.styles as CSSResultGroup\n\n /** 담는 페이지. 이 부품은 그것의 `renderPickerBody()` 를 그린다. */\n @property({ type: Object }) page?: any\n\n private searchInput?: HTMLInputElement\n\n private readonly onSearchInput = (event: Event) => {\n /* ox-input-search 는 값을 확정할 때만 host `change`를 낸다. 모듈을 찾는 일은 입력 중에도\n 결과가 바뀌어야 하므로, 이 팝업의 shadow root 안 native input을 직접 듣는다. */\n this.page?.updateKeyword?.(event)\n }\n\n connectedCallback() {\n super.connectedCallback()\n if (this.page) {\n this.page.picker = this\n }\n }\n\n disconnectedCallback() {\n this.searchInput?.removeEventListener('input', this.onSearchInput)\n this.searchInput = undefined\n super.disconnectedCallback()\n if (this.page?.picker === this) {\n this.page.picker = undefined\n }\n }\n\n render() {\n return this.page?.renderPickerBody?.() ?? html``\n }\n\n updated() {\n const nextSearchInput = (this.renderRoot.querySelector('ox-input-search') as any)?.shadowRoot?.querySelector(\n 'input'\n ) as HTMLInputElement | undefined\n\n if (nextSearchInput === this.searchInput) {\n return\n }\n\n this.searchInput?.removeEventListener('input', this.onSearchInput)\n this.searchInput = nextSearchInput\n this.searchInput?.addEventListener('input', this.onSearchInput)\n }\n}\n"]}
|
|
@@ -46,6 +46,7 @@ export declare class RolePrivilegePage extends RolePrivilegePage_base {
|
|
|
46
46
|
private mySubdomain;
|
|
47
47
|
private fetchState;
|
|
48
48
|
private saving;
|
|
49
|
+
private dirty;
|
|
49
50
|
/**
|
|
50
51
|
* 부모 도메인의 역할은 여기서 못 고친다 — `updateRole` 의 범위가 `domain.id` 하나다.
|
|
51
52
|
* 목록이 상속으로 보여 주므로 관리자가 여기까지 온다. 읽기 전용으로 두고 어디로 가야 하는지
|
|
@@ -54,6 +55,18 @@ export declare class RolePrivilegePage extends RolePrivilegePage_base {
|
|
|
54
55
|
private get inherited();
|
|
55
56
|
private get savable();
|
|
56
57
|
private get visibleModules();
|
|
58
|
+
/**
|
|
59
|
+
* `role-privilege-picker` 는 이 페이지의 상태를 읽어 그린다. 둘은 서로 다른 shadow root 이므로
|
|
60
|
+
* 여기 state 만 바꾸면 picker 는 다시 그릴 이유를 알지 못한다. 검색·모듈 선택은 그 누락 때문에
|
|
61
|
+
* 눌러도 화면이 그대로였다.
|
|
62
|
+
*/
|
|
63
|
+
private refreshPicker;
|
|
64
|
+
private selectModule;
|
|
65
|
+
/**
|
|
66
|
+
* ox-input-search 의 `change` 는 입력을 끝내고 포커스를 옮길 때만 온다. 모듈을 찾는 칸은
|
|
67
|
+
* 입력하는 즉시 좁혀져야 하므로 shadow 경계를 넘어오는 native input 이벤트도 함께 받는다.
|
|
68
|
+
*/
|
|
69
|
+
private updateKeyword;
|
|
57
70
|
get context(): {
|
|
58
71
|
title: {
|
|
59
72
|
icon: string;
|
|
@@ -62,6 +75,8 @@ export declare class RolePrivilegePage extends RolePrivilegePage_base {
|
|
|
62
75
|
help: string;
|
|
63
76
|
actions: {
|
|
64
77
|
title: string;
|
|
78
|
+
disabled: boolean;
|
|
79
|
+
disabledTitle: string;
|
|
65
80
|
action: () => Promise<void>;
|
|
66
81
|
}[];
|
|
67
82
|
};
|
|
@@ -168,6 +183,9 @@ export declare class RolePrivilegePage extends RolePrivilegePage_base {
|
|
|
168
183
|
/** 평평한 목록을 모듈로 접는다. 축 순서는 아는 것 먼저, 나머지는 이름 순. */
|
|
169
184
|
private build;
|
|
170
185
|
private onToggle;
|
|
186
|
+
/** 라벨·설명을 포함한 행 전체를 누를 수 있게 한다. checkbox와 키보드 Space는 native change가 맡는다. */
|
|
187
|
+
private toggleCellFromRow;
|
|
188
|
+
canDeactivate(): Promise<boolean>;
|
|
171
189
|
/**
|
|
172
190
|
* 저장.
|
|
173
191
|
*
|
|
@@ -8,9 +8,11 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|
|
8
8
|
import { client, gqlContext } from '@operato/graphql';
|
|
9
9
|
import { i18next, localize } from '@operato/i18n';
|
|
10
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 = {
|
|
@@ -63,6 +65,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
63
65
|
this.mySubdomain = '';
|
|
64
66
|
this.fetchState = 'loading';
|
|
65
67
|
this.saving = false;
|
|
68
|
+
this.dirty = false;
|
|
66
69
|
}
|
|
67
70
|
static { this.styles = [
|
|
68
71
|
ScrollbarStyles,
|
|
@@ -198,6 +201,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
198
201
|
gap: 4px 10px;
|
|
199
202
|
padding: 12px 14px;
|
|
200
203
|
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
|
204
|
+
cursor: pointer;
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
ul[axes] li:last-child {
|
|
@@ -214,6 +218,39 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
214
218
|
cursor: default;
|
|
215
219
|
}
|
|
216
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
|
+
|
|
217
254
|
/* 설명은 **본문**이다 — tooltip 이 아니다. 체크박스 아래 칸을 비워 두고 거기 놓는다. */
|
|
218
255
|
ul[axes] [says] {
|
|
219
256
|
grid-column: 2;
|
|
@@ -435,8 +472,35 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
435
472
|
return this.fetchState === 'loaded' && !this.saving && !this.inherited;
|
|
436
473
|
}
|
|
437
474
|
get visibleModules() {
|
|
438
|
-
|
|
439
|
-
|
|
475
|
+
return filterPrivilegeModules(this.modules, this.keyword, moduleLabel);
|
|
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();
|
|
440
504
|
}
|
|
441
505
|
/*
|
|
442
506
|
* The page is named after the screen, not after the role it happens to be showing.
|
|
@@ -459,6 +523,8 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
459
523
|
: [
|
|
460
524
|
{
|
|
461
525
|
title: i18next.t('button.save'),
|
|
526
|
+
disabled: !this.dirty || !this.savable,
|
|
527
|
+
disabledTitle: this.saving ? '권한을 저장하고 있습니다.' : '권한을 바꾼 뒤 저장할 수 있습니다.',
|
|
462
528
|
action: this.onSave.bind(this)
|
|
463
529
|
}
|
|
464
530
|
]
|
|
@@ -639,7 +705,8 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
639
705
|
<ox-input-search
|
|
640
706
|
.placeholder=${i18next.t('text.filter by module name')}
|
|
641
707
|
.value=${this.keyword}
|
|
642
|
-
@
|
|
708
|
+
@input=${(event) => this.updateKeyword(event)}
|
|
709
|
+
@change=${(event) => this.updateKeyword(event)}
|
|
643
710
|
></ox-input-search>
|
|
644
711
|
<div scroll>${this.renderModules()}</div>
|
|
645
712
|
</div>
|
|
@@ -652,6 +719,18 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
652
719
|
<div scroll>${this.renderAxes()}</div>
|
|
653
720
|
</div>
|
|
654
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>
|
|
733
|
+
</div>
|
|
655
734
|
`;
|
|
656
735
|
}
|
|
657
736
|
/**
|
|
@@ -686,7 +765,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
686
765
|
${this.visibleModules.map(module => {
|
|
687
766
|
const on = module.cells.filter(cell => this.assigned.has(cell.id)).length;
|
|
688
767
|
return html `
|
|
689
|
-
<li ?selected=${module.category === this.selected} @click=${() =>
|
|
768
|
+
<li ?selected=${module.category === this.selected} @click=${() => this.selectModule(module.category)}>
|
|
690
769
|
<span>${moduleLabel(module.category)}</span>
|
|
691
770
|
${on
|
|
692
771
|
? html `<span on>${i18next.t('text.x on', { x: on })}</span>`
|
|
@@ -711,7 +790,11 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
711
790
|
${module.cells.map(cell => {
|
|
712
791
|
const on = this.assigned.has(cell.id);
|
|
713
792
|
return html `
|
|
714
|
-
<li
|
|
793
|
+
<li
|
|
794
|
+
?ghost=${cell.deprecated}
|
|
795
|
+
?readonly=${this.inherited || (cell.deprecated && !on)}
|
|
796
|
+
@click=${(event) => this.toggleCellFromRow(event, cell, on)}
|
|
797
|
+
>
|
|
715
798
|
<input
|
|
716
799
|
type="checkbox"
|
|
717
800
|
id="cell-${cell.id}"
|
|
@@ -719,15 +802,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
719
802
|
?disabled=${this.inherited || (cell.deprecated && !on)}
|
|
720
803
|
@change=${(e) => this.onToggle(cell.id, e.target.checked)}
|
|
721
804
|
/>
|
|
722
|
-
<label
|
|
723
|
-
for="cell-${cell.id}"
|
|
724
|
-
@click=${() => {
|
|
725
|
-
if (!this.inherited && !(cell.deprecated && !on)) {
|
|
726
|
-
this.onToggle(cell.id, !on);
|
|
727
|
-
}
|
|
728
|
-
}}
|
|
729
|
-
>${axisLabel(cell.axis)}</label
|
|
730
|
-
>
|
|
805
|
+
<label for="cell-${cell.id}">${axisLabel(cell.axis)}</label>
|
|
731
806
|
|
|
732
807
|
${cell.deprecated
|
|
733
808
|
? html `<div says>${i18next.t('text.privilege no longer declared')}</div>`
|
|
@@ -832,17 +907,43 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
832
907
|
}
|
|
833
908
|
this.modules = [...byCategory.values()].sort((a, b) => moduleLabel(a.category).localeCompare(moduleLabel(b.category)));
|
|
834
909
|
this.assigned = assigned;
|
|
910
|
+
this.dirty = false;
|
|
835
911
|
/* 고른 것이 없으면 첫 모듈을 연다 — 빈 오른쪽 판을 처음 화면으로 두지 않는다. */
|
|
836
912
|
if (!this.modules.find(m => m.category === this.selected)) {
|
|
837
913
|
this.selected = this.modules[0]?.category || '';
|
|
838
914
|
}
|
|
839
915
|
}
|
|
840
916
|
onToggle(id, checked) {
|
|
917
|
+
if (this.assigned.has(id) === checked) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
841
920
|
const next = new Set(this.assigned);
|
|
842
921
|
checked ? next.add(id) : next.delete(id);
|
|
843
922
|
this.assigned = next;
|
|
923
|
+
this.dirty = true;
|
|
924
|
+
this.updateContext();
|
|
844
925
|
/* 팝업은 자기 상태를 안 들고 이 페이지를 본다 — 열려 있으면 다시 그리라고 알린다. */
|
|
845
|
-
this.
|
|
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
|
+
});
|
|
846
947
|
}
|
|
847
948
|
/**
|
|
848
949
|
* 저장.
|
|
@@ -857,6 +958,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
857
958
|
return;
|
|
858
959
|
}
|
|
859
960
|
this.saving = true;
|
|
961
|
+
this.updateContext();
|
|
860
962
|
try {
|
|
861
963
|
const response = await client.mutate({
|
|
862
964
|
mutation: gql `
|
|
@@ -879,9 +981,11 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
879
981
|
document.dispatchEvent(new CustomEvent('notify', { detail: { message: i18next.t('text.data_updated_successfully') } }));
|
|
880
982
|
/* 유령을 뗐으면 목록에서 사라져야 하고, 켠 것이 요약에 반영돼야 한다. */
|
|
881
983
|
await this.fetch();
|
|
984
|
+
this.dirty = false;
|
|
882
985
|
}
|
|
883
986
|
finally {
|
|
884
987
|
this.saving = false;
|
|
988
|
+
this.updateContext();
|
|
885
989
|
}
|
|
886
990
|
}
|
|
887
991
|
};
|
|
@@ -925,6 +1029,10 @@ __decorate([
|
|
|
925
1029
|
state(),
|
|
926
1030
|
__metadata("design:type", Boolean)
|
|
927
1031
|
], RolePrivilegePage.prototype, "saving", void 0);
|
|
1032
|
+
__decorate([
|
|
1033
|
+
state(),
|
|
1034
|
+
__metadata("design:type", Boolean)
|
|
1035
|
+
], RolePrivilegePage.prototype, "dirty", void 0);
|
|
928
1036
|
RolePrivilegePage = __decorate([
|
|
929
1037
|
customElement('role-privilege-page')
|
|
930
1038
|
], RolePrivilegePage);
|