@things-factory/auth-ui 10.1.3 → 10.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist-client/components/change-password.d.ts +11 -0
  2. package/dist-client/components/change-password.js +103 -6
  3. package/dist-client/components/change-password.js.map +1 -1
  4. package/dist-client/components/role-detail.d.ts +144 -0
  5. package/dist-client/components/role-detail.js +698 -0
  6. package/dist-client/components/role-detail.js.map +1 -0
  7. package/dist-client/components/user-detail.d.ts +151 -0
  8. package/dist-client/components/user-detail.js +631 -0
  9. package/dist-client/components/user-detail.js.map +1 -0
  10. package/dist-client/components/user-invite.d.ts +80 -0
  11. package/dist-client/components/user-invite.js +422 -0
  12. package/dist-client/components/user-invite.js.map +1 -0
  13. package/dist-client/entries/auth/activate.d.ts +0 -1
  14. package/dist-client/entries/auth/activate.js +0 -8
  15. package/dist-client/entries/auth/activate.js.map +1 -1
  16. package/dist-client/pages/role/role-list-page.d.ts +112 -0
  17. package/dist-client/pages/role/role-list-page.js +514 -0
  18. package/dist-client/pages/role/role-list-page.js.map +1 -0
  19. package/dist-client/pages/role/role-privilege-page.d.ts +123 -0
  20. package/dist-client/pages/role/role-privilege-page.js +801 -0
  21. package/dist-client/pages/role/role-privilege-page.js.map +1 -0
  22. package/dist-client/pages/user/user-list-page.d.ts +88 -0
  23. package/dist-client/pages/user/user-list-page.js +474 -0
  24. package/dist-client/pages/user/user-list-page.js.map +1 -0
  25. package/dist-client/route.js +5 -2
  26. package/dist-client/route.js.map +1 -1
  27. package/dist-client/tsconfig.tsbuildinfo +1 -1
  28. package/dist-server/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +4 -4
  30. package/things-factory.config.js +3 -2
  31. package/translations/en.json +46 -3
  32. package/translations/ja.json +0 -2
  33. package/translations/ko.json +47 -4
  34. package/translations/ms.json +0 -2
  35. package/translations/zh.json +0 -2
  36. package/dist-client/components/contact-us.d.ts +0 -20
  37. package/dist-client/components/contact-us.js +0 -121
  38. package/dist-client/components/contact-us.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-privilege-page.js","sourceRoot":"","sources":["../../../client/pages/role/role-privilege-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/textfield/filled-text-field.js'\nimport '@material/web/checkbox/checkbox.js'\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, state } from 'lit/decorators.js'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { PageView } from '@operato/shell'\nimport { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'\nimport { getDomain } from '@things-factory/auth-base/dist-client'\n\n/** 아는 축은 사람 말로 쓰고, 그 순서로 세운다. 모르는 축은 자기 이름으로 뒤에 붙는다. */\nconst KNOWN_AXES = ['query', 'mutation', 'execute']\nconst AXIS_LABEL: Record<string, string> = {\n query: 'label.read',\n mutation: 'label.write',\n execute: 'label.execute'\n}\n\ntype Cell = { id: string; axis: string; deprecated: boolean; description?: string }\ntype Module = { category: string; cells: Cell[] }\n\n/**\n * 한 역할이 무엇을 열 수 있나 — **왼쪽에 모듈, 오른쪽에 그 모듈에서 되는 일.**\n *\n * ── 왜 페이지인가 (UX 레인, 2026-09-10) ─────────────────────────────────────\n * 처음에는 이것을 팝업에 넣었고 사용자에게 「팝업이 너무 작다」를 들었다. 그때 `size` 를 키우는\n * 것으로 답했는데 그것이 증상 처리였다 — **모듈이 46개고 각 줄에 설명 문장이 붙으면 팝업에\n * 안 들어간다.** 자리 자체가 틀렸다.\n *\n * 그리고 이 저장소의 관례가 그렇다: `editable` 컬럼 139 파일 대 `openPopup` 96 파일 —\n * **격자에서 고치고, 팝업은 격자가 못 담는 것에 쓴다.** 역할의 이름과 설명은 목록 격자에서\n * 고치고(`updateMultipleRole`), 권한은 격자가 못 담으므로 이 페이지가 받는다.\n *\n * ── 「배울 것이 없다」를 고치는 자리 ────────────────────────────────────────\n * 사용자 평가가 이랬다 — **「화면으로부터 배울 수 있는 것이 없고, 혼란스럽고, 어렵고, 불편하다」.**\n * 옛 격자는 `board` 라고만 쓰고 「쓰기를 켜면 이 사람이 무엇을 할 수 있게 되나」를 어디에도 안\n * 적었다. 설명을 체크박스 `title` 에 숨겨 놨는데 마우스를 올려야 나오고, 그건 배우는 길이 아니다.\n *\n * 그래서 **설명은 본문이다.** 체크박스 옆에 문장이 함께 선다.\n *\n * ⚠ **그런데 그 문장이 아직 시스템에 없다.** `Privilege.description` 은 칸도 있고\n * `createPrivilege` 가 받지만, 부팅 동기화는 `{ category, name }` 둘만 저장하고\n * (`auth-base/server/index.ts:34-36`) 그 mutation 을 부르는 화면이 없다 — 713개 선언이 전부\n * 빈 설명이다. 그래서 이 화면은 **없으면 없다고 말한다.** 코드 낱말(`board mutation`)을 다시\n * 보여 주지 않고, 지어내지도 않는다. 글은 UX 레인이 `privilege.category.*` 키로 채우는 중이다.\n *\n * ── 저장 범위는 서버가 준 목록이다 ──────────────────────────────────────────\n * 옛 편집기는 `querySelectorAll('input:checked')` 로 보냈다. 왼쪽에서 다른 모듈을 고르면\n * 앞 모듈의 체크박스는 **DOM 에 없다** — 그 방식이면 모듈을 옮겨 다니는 것만으로 권한이\n * 조용히 떨어진다. 여기서는 `rolePrivileges` 가 준 전체가 범위이고, 켠 것은 `assigned` 에만\n * 있다. 화면이 무엇을 그렸는지와 무관하다.\n */\n@customElement('role-privilege-page')\nexport class RolePrivilegePage extends localize(i18next)(PageView) {\n static styles: CSSResultGroup = [\n ScrollbarStyles,\n CommonHeaderStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-height: 0;\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n\n [body] {\n flex: 1;\n min-height: 0;\n display: grid;\n /* 왼쪽은 이름만 담으므로 고정, 오른쪽이 문장을 받으므로 남는 폭을 준다. */\n grid-template-columns: minmax(200px, 280px) 1fr;\n gap: var(--spacing-large, 12px);\n padding: var(--spacing-large, 12px);\n }\n\n @media screen and (max-width: 780px) {\n [body] {\n /* 좁으면 위아래로. 고른 모듈이 아래에 오는 것이 읽는 순서와 같다. */\n grid-template-columns: 1fr;\n grid-template-rows: auto 1fr;\n }\n }\n\n [panel] {\n display: flex;\n flex-direction: column;\n min-height: 0;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: var(--md-sys-shape-corner-medium, 10px);\n background-color: var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));\n overflow: hidden;\n }\n\n [panel] > header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n }\n\n [panel] > header [count] {\n color: var(--md-sys-color-outline);\n }\n\n [scroll] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n }\n\n /*\n 입력 상자는 md-filled-text-field 다 — 생 input 이 아니다.\n 같은 일을 하는 create-role.ts·create-user.ts 가 이것을 쓰고, 이 꾸러미에서 10개\n 파일이 그렇다(UX 레인이 재서 바로잡아 준 것). 생 input 은 비밀번호·프로필 셋뿐이다.\n */\n md-filled-text-field {\n margin: 8px 12px;\n width: calc(100% - 24px);\n }\n\n /* ── 왼쪽: 모듈 ──────────────────────────────────────────────────────── */\n ul[modules] {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n ul[modules] li {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n padding: 8px 12px;\n cursor: pointer;\n border-left: 3px solid transparent;\n }\n\n ul[modules] li:hover {\n background-color: var(--md-sys-color-surface-variant);\n }\n\n /* 고른 것은 면과 왼쪽 선으로 말한다 — 글자색만 바꾸면 야간에 안 보인다. */\n ul[modules] li[selected] {\n border-left-color: var(--md-sys-color-primary);\n background-color: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n\n ul[modules] li [on] {\n font-size: 0.78rem;\n color: var(--md-sys-color-primary);\n }\n\n ul[modules] li[selected] [on] {\n color: var(--md-sys-color-on-secondary-container);\n }\n\n ul[modules] li [none] {\n font-size: 0.78rem;\n color: var(--md-sys-color-outline);\n }\n\n /* ── 오른쪽: 그 모듈에서 되는 일 ────────────────────────────────────── */\n ul[axes] {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n ul[axes] li {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 4px 10px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--md-sys-color-surface-variant);\n }\n\n ul[axes] li:last-child {\n border-bottom: none;\n }\n\n ul[axes] label {\n font-size: 0.95rem;\n font-weight: 600;\n cursor: pointer;\n }\n\n ul[axes] li[ghost] label {\n cursor: default;\n }\n\n /* 설명은 **본문**이다 — tooltip 이 아니다. 체크박스 아래 칸을 비워 두고 거기 놓는다. */\n ul[axes] [says] {\n grid-column: 2;\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.85rem;\n line-height: 1.55;\n }\n\n ul[axes] [says][missing] {\n color: var(--md-sys-color-outline);\n font-style: italic;\n }\n\n ul[axes] li[ghost] label {\n text-decoration: line-through;\n color: var(--md-sys-color-outline);\n }\n\n /*\n Which role is this? The header alone was not enough - it is one line of chrome above a\n page full of module names, and a role carries a description that says what the job is.\n Name and description sit in the body, where the reader is already looking.\n */\n [subject] {\n margin: var(--spacing-large, 12px) var(--spacing-large, 12px) 0;\n }\n\n [subject] [name] {\n font-size: 1.15rem;\n font-weight: 640;\n }\n\n [subject] [says] {\n margin-top: 2px;\n font-size: 0.88rem;\n line-height: 1.55;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n [subject] [says][missing] {\n color: var(--md-sys-color-outline);\n font-style: italic;\n }\n\n /*\n What is on, all together. The two panels answer \"what can this role do in one module\";\n they cannot answer \"what does this role do\", because that reading needs 46 clicks.\n */\n [granted] {\n margin: var(--spacing-medium, 8px) var(--spacing-large, 12px) 0;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: var(--md-sys-shape-corner-medium, 10px);\n background-color: var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));\n }\n\n [granted] > header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n padding: 8px 12px;\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n }\n\n [granted] > header [count] {\n color: var(--md-sys-color-outline);\n }\n\n [granted] ul {\n display: flex;\n flex-wrap: wrap;\n gap: 4px 6px;\n list-style: none;\n margin: 0;\n padding: 0 12px 10px;\n /* Long-running roles reach every module; cap the block so the panels stay on screen. */\n max-height: 22vh;\n overflow: auto;\n }\n\n [granted] li {\n display: inline-flex;\n align-items: baseline;\n gap: 5px;\n padding: 3px 9px;\n border-radius: var(--md-sys-shape-corner-small, 6px);\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.82rem;\n cursor: pointer;\n }\n\n [granted] li:hover {\n background-color: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n\n [granted] li [axes] {\n color: var(--md-sys-color-primary);\n }\n\n [granted] li:hover [axes] {\n color: var(--md-sys-color-on-secondary-container);\n }\n\n [granted] [empty] {\n padding: 0 12px 10px;\n }\n\n [note] {\n margin: var(--spacing-large, 12px);\n padding: 10px 12px;\n border-radius: var(--md-sys-shape-corner-small, 6px);\n font-size: 0.85rem;\n line-height: 1.55;\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n [note][attention] {\n background-color: var(--md-sys-color-error-container);\n color: var(--md-sys-color-on-error-container);\n }\n\n [empty] {\n padding: var(--spacing-large, 12px);\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.85rem;\n }\n `\n ]\n\n /** 셸이 `role-privileges/<id>` 의 뒤를 `lifecycle.resourceId` 로 준다. */\n @property({ type: Object }) lifecycle: any\n\n /*\n * 이름을 `role` 로 두면 안 된다 — `HTMLElement` 가 ARIA 반영으로 `role` 을 이미 갖고 있어서\n * private 필드로 가리면 `CustomElementClass` 에 안 맞는다(TS1238·TS2415).\n */\n @state() private subject: any = {}\n @state() private modules: Module[] = []\n @state() private assigned: Set<string> = new Set()\n @state() private holders: any[] = []\n @state() private selected: string = ''\n @state() private keyword: string = ''\n @state() private mySubdomain: string = ''\n\n @state() private fetchState: 'loading' | 'failed' | 'loaded' = 'loading'\n @state() private saving: boolean = false\n\n /**\n * 부모 도메인의 역할은 여기서 못 고친다 — `updateRole` 의 범위가 `domain.id` 하나다.\n * 목록이 상속으로 보여 주므로 관리자가 여기까지 온다. 읽기 전용으로 두고 어디로 가야 하는지\n * 말한다. (프로필의 `domain` 에 `id` 가 없어서 `subdomain` 으로 견준다.)\n */\n private get inherited(): boolean {\n const subdomain = this.subject?.domain?.subdomain\n return Boolean(subdomain && this.mySubdomain && subdomain !== this.mySubdomain)\n }\n\n private get savable(): boolean {\n return this.fetchState === 'loaded' && !this.saving && !this.inherited\n }\n\n private get visibleModules(): Module[] {\n const keyword = this.keyword.trim().toLowerCase()\n return keyword ? this.modules.filter(m => m.category.toLowerCase().includes(keyword)) : this.modules\n }\n\n get context() {\n return {\n title: this.subject?.name || i18next.t('field.privileges'),\n help: 'auth/roles',\n actions: this.inherited\n ? []\n : [\n {\n title: i18next.t('button.save'),\n action: this.onSave.bind(this)\n }\n ]\n }\n }\n\n render() {\n if (this.fetchState === 'failed') {\n return html`<ox-oops-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 return html`\n ${this.inherited\n ? html`<div note attention>\n ${i18next.t('text.role belongs to x domain and is edited there', {\n x: this.subject?.domain?.name || this.subject?.domain?.subdomain\n })}\n </div>`\n : nothing}\n ${this.renderSubject()} ${this.renderGranted()} ${this.renderHolders()}\n\n <div body>\n <div panel>\n <header>\n ${i18next.t('field.module')}\n <span count>${this.modules.length}</span>\n </header>\n <md-filled-text-field\n type=\"text\"\n label=${i18next.t('text.filter by module name')}\n .value=${this.keyword}\n @input=${(e: Event) => (this.keyword = (e.target as HTMLInputElement).value)}\n ></md-filled-text-field>\n <div scroll>${this.renderModules()}</div>\n </div>\n\n <div panel>\n <header>\n ${this.selected ? moduleLabel(this.selected) : i18next.t('field.privileges')}\n <span count>${i18next.t('text.x of y on', { x: this.assigned.size, y: this.cellCount() })}</span>\n </header>\n <div scroll>${this.renderAxes()}</div>\n </div>\n </div>\n `\n }\n\n /**\n * Which role am I editing, and what is it for.\n *\n * The page header carried the name and nothing else, and the description - the one line that\n * says what the job is - was only visible back on the list. A reader who arrives by link had\n * no way to tell \"Administrator\" from \"PLANT-MANAGER\" beyond the word itself.\n *\n * A role with no description says so. The next thing to do is write one, in the list grid.\n */\n private renderSubject() {\n if (this.fetchState !== 'loaded') {\n return nothing\n }\n\n return html`\n <div subject>\n <div name>${this.subject?.name || i18next.t('label.role')}</div>\n ${this.subject?.description\n ? html`<div says>${this.subject.description}</div>`\n : html`<div says missing>${i18next.t('text.no description for this role')}</div>`}\n </div>\n `\n }\n\n /**\n * Everything this role has, in one block.\n *\n * The two panels answer \"what can this role do in this module\". They cannot answer \"what does\n * this role do at all\" - that reading costs one click per module, 46 of them, and the person\n * granting a role needs the whole answer before they grant it.\n *\n * Only what is on. The full matrix is what the panels are for, and repeating it here would\n * put the same 46 modules on screen twice.\n *\n * Each entry moves the panels to that module, so this block doubles as the way in.\n */\n private renderGranted() {\n if (this.fetchState !== 'loaded' || !this.modules.length) {\n return nothing\n }\n\n const on = this.modules\n .map(module => ({\n category: module.category,\n axes: module.cells.filter(cell => this.assigned.has(cell.id))\n }))\n .filter(entry => entry.axes.length)\n\n return html`\n <div granted>\n <header>\n ${i18next.t('field.granted privileges')}\n <span count>${i18next.t('text.x of y on', { x: this.assigned.size, y: this.cellCount() })}</span>\n </header>\n ${on.length\n ? html`<ul>\n ${on.map(\n entry => html`<li\n title=${i18next.t('text.pick a module on the left')}\n @click=${() => (this.selected = entry.category)}\n >\n ${moduleLabel(entry.category)}\n <span axes>${entry.axes.map(cell => axisLabel(cell.axis)).join(' · ')}</span>\n </li>`\n )}\n </ul>`\n : html`<div empty>${i18next.t('text.nothing is on for this role yet')}</div>`}\n </div>\n `\n }\n\n /**\n * 이 역할을 가진 사람.\n *\n * 권한을 켜기 **전에** 누가 영향을 받는지 보이는 자리다. 아무도 안 가진 역할을 고치는 것과\n * 40명이 가진 역할을 고치는 것은 다른 일인데, 옛 화면은 그 둘이 똑같이 보였다.\n */\n private renderHolders() {\n if (this.fetchState !== 'loaded') {\n return nothing\n }\n\n return this.holders.length\n ? html`<div note>\n ${i18next.t('text.x people hold this role', { x: this.holders.length })}\n — ${this.holders.map(h => h.name || h.email).join(', ')}\n </div>`\n : html`<div note>${i18next.t('text.no one holds this role yet')}</div>`\n }\n\n private renderModules() {\n if (this.fetchState === 'loading') {\n return html`<div empty>${i18next.t('text.loading')}</div>`\n }\n\n if (!this.modules.length) {\n return html`<div empty>${i18next.t('text.no privilege declared in this deployment')}</div>`\n }\n\n if (!this.visibleModules.length) {\n return html`<div empty>${i18next.t('text.nothing matches x', { x: this.keyword })}</div>`\n }\n\n return html`\n <ul modules>\n ${this.visibleModules.map(module => {\n const on = module.cells.filter(cell => this.assigned.has(cell.id)).length\n\n return html`\n <li ?selected=${module.category === this.selected} @click=${() => (this.selected = module.category)}>\n <span>${moduleLabel(module.category)}</span>\n ${on\n ? html`<span on>${i18next.t('text.x on', { x: on })}</span>`\n : html`<span none>${i18next.t('text.none on')}</span>`}\n </li>\n `\n })}\n </ul>\n `\n }\n\n private renderAxes() {\n if (this.fetchState === 'loading') {\n return html`<div empty>${i18next.t('text.loading')}</div>`\n }\n\n const module = this.modules.find(m => m.category === this.selected)\n if (!module) {\n /* 아무것도 안 고른 상태 — 무엇을 해야 하는지 말한다. */\n return html`<div empty>${i18next.t('text.pick a module on the left')}</div>`\n }\n\n return html`\n <ul axes>\n ${module.cells.map(cell => {\n const on = this.assigned.has(cell.id)\n\n return html`\n <li ?ghost=${cell.deprecated}>\n <md-checkbox\n id=\"cell-${cell.id}\"\n touch-target=\"wrapper\"\n .checked=${on}\n ?disabled=${this.inherited || (cell.deprecated && !on)}\n @change=${(e: Event) => this.onToggle(cell.id, (e.target as HTMLInputElement).checked)}\n ></md-checkbox>\n <!--\n md-checkbox 는 native input 이 아니라서 label for 로는 안 눌린다.\n 글자를 눌러도 켜지는 것이 사람이 기대하는 것이므로 같은 처리기를 붙인다.\n -->\n <label\n for=\"cell-${cell.id}\"\n @click=${() => {\n if (!this.inherited && !(cell.deprecated && !on)) {\n this.onToggle(cell.id, !on)\n }\n }}\n >${axisLabel(cell.axis)}</label\n >\n\n ${cell.deprecated\n ? html`<div says>${i18next.t('text.privilege no longer declared')}</div>`\n : axisSays(module.category, cell) ||\n html`<div says missing>${i18next.t('text.no description for this privilege')}</div>`}\n </li>\n `\n })}\n </ul>\n `\n }\n\n private cellCount(): number {\n return this.modules.reduce((sum, module) => sum + module.cells.length, 0)\n }\n\n async pageUpdated(changes: any, lifecycle: any) {\n if (this.active) {\n this.lifecycle = lifecycle\n await this.fetch()\n }\n }\n\n /**\n * 한 번의 질의로 **전체 권한과 이 역할이 가진 것**을 함께 읽는다(`rolePrivileges(roleId:)`).\n *\n * 역할 이름과 가진 사람은 `roles(filters: id)` 로 같은 문서에서 받는다 — 옛 편집기는 역할을\n * **이름**으로 다시 찾았고, 부모 도메인에 같은 이름이 있으면 남의 역할을 그렸다.\n */\n private async fetch() {\n const roleId = this.lifecycle?.resourceId\n if (!roleId) {\n this.fetchState = 'failed'\n return\n }\n\n this.fetchState = 'loading'\n this.mySubdomain = (await getDomain())?.subdomain || ''\n\n const response = await client.query({\n query: gql`\n query ($roleId: String!, $filters: [Filter!]) {\n rolePrivileges(roleId: $roleId) {\n id\n name\n category\n description\n assigned\n deprecated\n }\n roles(filters: $filters) {\n items {\n id\n name\n description\n domain {\n id\n name\n subdomain\n }\n users {\n id\n name\n email\n }\n }\n }\n }\n `,\n variables: { roleId, filters: [{ name: 'id', operator: 'eq', value: roleId }] },\n context: gqlContext()\n })\n\n if (response.errors?.length) {\n /* 실패를 「권한이 하나도 없다」로 그리지 않는다 — 저장하면 실제로 그렇게 된다. */\n this.fetchState = 'failed'\n return\n }\n\n this.subject = response.data?.roles?.items?.[0] || {}\n this.holders = this.subject.users || []\n this.build(response.data?.rolePrivileges || [])\n this.fetchState = 'loaded'\n\n /*\n * The shell reads `context` once, when the page becomes active - before this fetch\n * resolves. Without this call the header keeps the fallback, so the screen showed\n * \"권한\" and never said which role was being edited.\n */\n this.updateContext()\n }\n\n /** 평평한 목록을 모듈로 접는다. 축 순서는 아는 것 먼저, 나머지는 이름 순. */\n private build(privileges: any[]) {\n const byCategory = new Map<string, Module>()\n const assigned = new Set<string>()\n\n for (const privilege of privileges) {\n const { id, name, category, description, deprecated } = privilege\n if (!id || !name || !category) {\n continue\n }\n\n if (privilege.assigned) {\n assigned.add(id)\n }\n\n const module = byCategory.get(category) || { category, cells: [] as Cell[] }\n module.cells.push({ id, axis: name, deprecated: Boolean(deprecated), description })\n byCategory.set(category, module)\n }\n\n for (const module of byCategory.values()) {\n module.cells.sort((a, b) => axisOrder(a.axis) - axisOrder(b.axis) || a.axis.localeCompare(b.axis))\n }\n\n this.modules = [...byCategory.values()].sort((a, b) => moduleLabel(a.category).localeCompare(moduleLabel(b.category)))\n this.assigned = assigned\n\n /* 고른 것이 없으면 첫 모듈을 연다 — 빈 오른쪽 판을 처음 화면으로 두지 않는다. */\n if (!this.modules.find(m => m.category === this.selected)) {\n this.selected = this.modules[0]?.category || ''\n }\n }\n\n private onToggle(id: string, checked: boolean) {\n const next = new Set(this.assigned)\n checked ? next.add(id) : next.delete(id)\n this.assigned = next\n }\n\n /**\n * 저장.\n *\n * **범위는 `modules` 전체다.** 왼쪽에서 다른 모듈을 고르면 앞 모듈의 체크박스는 DOM 에\n * 없는데, 그것을 훑어 보내던 것이 옛 편집기가 권한을 조용히 떼던 자리다.\n *\n * `privileges` 만 보낸다 — 이름과 설명은 목록 격자가 소유한다.\n */\n private async onSave() {\n if (!this.savable) {\n return\n }\n\n this.saving = true\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: RolePatch!) {\n updateRole(id: $id, patch: $patch) {\n id\n }\n }\n `,\n variables: {\n id: this.subject.id,\n patch: { privileges: [...this.assigned].map(id => ({ id })) }\n },\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 document.dispatchEvent(\n new CustomEvent('notify', { detail: { message: i18next.t('text.data_updated_successfully') } })\n )\n\n /* 유령을 뗐으면 목록에서 사라져야 하고, 켠 것이 요약에 반영돼야 한다. */\n await this.fetch()\n } finally {\n this.saving = false\n }\n }\n}\n\n/**\n * 모듈 이름을 사람 말로.\n *\n * `privilege.category.<category>` 키가 있으면 그것을 쓰고, 없으면 **코드 낱말을 그대로** 낸다 —\n * 지어내지 않는다. 지금 46개 중 하나(`system`)만 채워져 있고, UX 레인이 나머지를 쓰고 있다.\n */\nfunction moduleLabel(category: string): string {\n const key = `privilege.category.${category}`\n return i18next.t(key, { defaultValue: category })\n}\n\nfunction axisLabel(axis: string): string {\n return AXIS_LABEL[axis] ? i18next.t(AXIS_LABEL[axis]) : axis\n}\n\n/**\n * 「켜면 무엇을 할 수 있게 되나」 — **이 화면의 이유다.**\n *\n * 사용자 평가가 「화면으로부터 배울 수 있는 것이 없다」였고, 그 원인은 화면이 감춘 것이 아니라\n * 시스템에 그 문장이 없던 것이었다. 부팅 동기화가 `{ category, name }` 둘만 저장해서\n * (`auth-base/server/index.ts:34-36`) 713개 선언이 전부 빈 설명이었다.\n *\n * 그 글을 UX 레인이 `privilege.allows.<category>.<axis>` 로 썼다(57 모듈 · 104 문장, ko·en).\n * 번역은 디스크에서 서비스되므로 **배포를 기다리지 않고 붙는다.**\n *\n * ── 보는 순서 ───────────────────────────────────────────────────────────────\n * ```\n * 1 서버 칸 채워지면 그것이 이긴다 — 배포마다 다를 수 있는 사실은 서버가 안다\n * 2 우리가 쓴 글 privilege.allows.<category>.<axis>\n * 3 없다고 말한다 둘 다 없으면. 지어내지 않는다\n * ```\n *\n * ⚠ **`defaultValue: ''` 가 없으면 3번으로 못 떨어진다** — i18next 는 키가 없을 때 **키 이름을\n * 그대로** 돌려주므로 화면에 `privilege.allows.board.query` 가 찍힌다. 빈 문자열을 기본값으로\n * 줘서 「없음」이 거짓으로 판정되게 한다.\n */\nfunction axisSays(category: string, cell: Cell) {\n if (cell.description) {\n return html`<div says>${cell.description}</div>`\n }\n\n const written = i18next.t(`privilege.allows.${category}.${cell.axis}`, { defaultValue: '' })\n return written ? html`<div says>${written}</div>` : null\n}\n\nfunction axisOrder(axis: string): number {\n const index = KNOWN_AXES.indexOf(axis)\n return index < 0 ? KNOWN_AXES.length : index\n}\n"]}
@@ -0,0 +1,88 @@
1
+ import '@material/web/icon/icon.js';
2
+ import '@operato/data-grist';
3
+ import '@operato/oops/ox-oops-note.js';
4
+ import '../../components/user-detail.js';
5
+ import '../../components/user-invite.js';
6
+ import type { CSSResultGroup } from 'lit';
7
+ import { PageView } from '@operato/shell';
8
+ import type { FetchOption } from '@operato/data-grist';
9
+ /**
10
+ * 사용자 목록 — **서버가 거르고, 서버가 쪽을 나눈다.**
11
+ *
12
+ * ── 옛 화면이 하던 것 (`pages/user/user-management.ts`) ──────────────────────
13
+ * `query { users { items { … } } }` 로 **도메인의 모든 사용자**를 한 번에 가져왔다. 한도도
14
+ * 서버 검색도 없었고, 프레임워크가 이미 그것을 경고하고 있었다
15
+ * (`unbounded list query on "User"`). 사용자는 권한(84행)과 달리 실제로 늘어난다.
16
+ *
17
+ * 그런데 **서버는 처음부터 받을 준비가 되어 있었다** — `users(filters:, pagination:, sortings:)`
18
+ * 가 `ListParam` 을 받고 `name`·`email`·`description` 을 검색어로 본다
19
+ * (`user-query.ts`). 화면이 안 쓰고 있었을 뿐이다.
20
+ *
21
+ * ── 왜 `ox-grist` 인가 ──────────────────────────────────────────────────────
22
+ * 손으로 만든 목록이 잃는 것을 그것이 이미 갖고 있다 — 쪽 나누기, 정렬, 컬럼 걸러 보기,
23
+ * 사람마다 기억하는 개인 설정, 카드↔표 모드, 내보내기. `figure-list-page` 가 같은 이유로
24
+ * 옮겼고 그 판단을 그대로 따른다.
25
+ *
26
+ * ── 옛 화면의 탭을 없앤 이유 ────────────────────────────────────────────────
27
+ * 탭이 `userType` 값으로 갈렸다. 그런데 그 칸은 `varchar` 이고 코드가 예상하는 넷
28
+ * (`admin`·`user`·`application`·`appliance`) 밖의 값이거나 비어 있으면 **그 사람이 어느 탭에도
29
+ * 안 나온다 — 화면에서 조용히 사라진다.**
30
+ *
31
+ * 여기서는 사람만 싣는다. 애플리케이션과 어플라이언스는 자기 화면(`applications`·`appliance`)에
32
+ * 있고, 거기서 자기 자격증명까지 함께 다루는 것이 맞다. 다만 **거르는 것은 서버가 해야 한다** —
33
+ * 지금은 `users` 가 종류를 안 가리므로 이 화면이 filter 로 요구한다.
34
+ */
35
+ declare const UserListPageBase: (new (...args: any[]) => {
36
+ __preferenceProviders: {
37
+ [element: string]: import("@operato/p13n").PagePreferenceProvider;
38
+ };
39
+ getPagePreferenceProvider(element: string): import("@operato/p13n").PagePreferenceProvider | undefined;
40
+ }) & (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
41
+ export declare class UserListPage extends UserListPageBase {
42
+ static styles: CSSResultGroup;
43
+ config: any;
44
+ mode: 'CARD' | 'GRID' | 'LIST';
45
+ private lockDurationMinutes;
46
+ private fetchState;
47
+ private total;
48
+ private filtered;
49
+ private grist;
50
+ get context(): {
51
+ title: string;
52
+ help: string;
53
+ };
54
+ render(): import("lit-html").TemplateResult<1>;
55
+ /**
56
+ * 목록 위에 겹쳐 그린다 — 격자를 떼면 다시 읽을 길이 없어진다.
57
+ *
58
+ * 거른 결과가 비어 있는 것은 **가르칠 자리가 아니다.** 그때는 격자가 스스로 「없음」을
59
+ * 말하게 두고, 여기서는 아무것도 그리지 않는다.
60
+ */
61
+ private renderNote;
62
+ pageInitialized(): Promise<void>;
63
+ pageUpdated(changes: any, lifecycle: any): Promise<void>;
64
+ /**
65
+ * 사람만 가져온다. **거르는 것은 서버가 한다** — 옛 화면은 전부 받아 클라이언트에서 나눴다.
66
+ *
67
+ * `userType` 은 `varchar` 라 값이 넷 밖으로 나갈 수 있다. 그래서 「기계가 아닌 것」을
68
+ * `not-in` 으로 요구한다 — 「사람인 것」을 `in` 으로 요구하면 값이 비어 있는 사람이 사라진다.
69
+ * (`userType` 을 enum 으로 못 박는 것은 §3 의 일이다.)
70
+ */
71
+ fetchHandler({ filters, page, limit, sortings }: FetchOption): Promise<{
72
+ records: any;
73
+ total: any;
74
+ }>;
75
+ /**
76
+ * 고른 사람의 역할을 고치는 자리를 연다.
77
+ *
78
+ * `domain-management` 가 격자 행에서 `ox-attribute-view` 를 여는 것과 같은 모양이다 —
79
+ * `handlers.click` 에서 `openPopup` 에 template 을 넘긴다.
80
+ *
81
+ * `escapable` 은 넘기지 않는다. 이 앱의 기본값이 `false` 이고, 그것이 곧 **ESC 에 먼저
82
+ * 묻는다**는 뜻이다 — 편집면에서 원하는 거동이 기본값이다.
83
+ */
84
+ private onSelectUser;
85
+ /** 목록의 역할 편집과 같은 모양으로 연다 — `openPopup` 에 template 을 넘긴다. */
86
+ private onInvite;
87
+ }
88
+ export {};
@@ -0,0 +1,474 @@
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 '../../components/user-detail.js';
6
+ import '../../components/user-invite.js';
7
+ import gql from 'graphql-tag';
8
+ import { css, html, nothing } from 'lit';
9
+ import { customElement, property, query, state } from 'lit/decorators.js';
10
+ import { client } from '@operato/graphql';
11
+ import { openPopup } from '@operato/layout';
12
+ import { i18next, localize } from '@operato/i18n';
13
+ import { p13n } from '@operato/p13n';
14
+ import { PageView } from '@operato/shell';
15
+ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles } from '@operato/styles';
16
+ import { isMobileDevice } from '@operato/utils';
17
+ /**
18
+ * 사용자 목록 — **서버가 거르고, 서버가 쪽을 나눈다.**
19
+ *
20
+ * ── 옛 화면이 하던 것 (`pages/user/user-management.ts`) ──────────────────────
21
+ * `query { users { items { … } } }` 로 **도메인의 모든 사용자**를 한 번에 가져왔다. 한도도
22
+ * 서버 검색도 없었고, 프레임워크가 이미 그것을 경고하고 있었다
23
+ * (`unbounded list query on "User"`). 사용자는 권한(84행)과 달리 실제로 늘어난다.
24
+ *
25
+ * 그런데 **서버는 처음부터 받을 준비가 되어 있었다** — `users(filters:, pagination:, sortings:)`
26
+ * 가 `ListParam` 을 받고 `name`·`email`·`description` 을 검색어로 본다
27
+ * (`user-query.ts`). 화면이 안 쓰고 있었을 뿐이다.
28
+ *
29
+ * ── 왜 `ox-grist` 인가 ──────────────────────────────────────────────────────
30
+ * 손으로 만든 목록이 잃는 것을 그것이 이미 갖고 있다 — 쪽 나누기, 정렬, 컬럼 걸러 보기,
31
+ * 사람마다 기억하는 개인 설정, 카드↔표 모드, 내보내기. `figure-list-page` 가 같은 이유로
32
+ * 옮겼고 그 판단을 그대로 따른다.
33
+ *
34
+ * ── 옛 화면의 탭을 없앤 이유 ────────────────────────────────────────────────
35
+ * 탭이 `userType` 값으로 갈렸다. 그런데 그 칸은 `varchar` 이고 코드가 예상하는 넷
36
+ * (`admin`·`user`·`application`·`appliance`) 밖의 값이거나 비어 있으면 **그 사람이 어느 탭에도
37
+ * 안 나온다 — 화면에서 조용히 사라진다.**
38
+ *
39
+ * 여기서는 사람만 싣는다. 애플리케이션과 어플라이언스는 자기 화면(`applications`·`appliance`)에
40
+ * 있고, 거기서 자기 자격증명까지 함께 다루는 것이 맞다. 다만 **거르는 것은 서버가 해야 한다** —
41
+ * 지금은 `users` 가 종류를 안 가리므로 이 화면이 filter 로 요구한다.
42
+ */
43
+ /*
44
+ * `as typeof PageView` 로 조이지 않는다 — 그렇게 하면 p13n 이 얹어 주는
45
+ * `getPagePreferenceProvider` 가 타입에서 사라져 개인 설정(모드·정렬·쪽 크기를 사람마다
46
+ * 기억하는 것)을 붙일 수 없다. `figure-list-page` 가 같은 주석을 달고 있다.
47
+ */
48
+ const UserListPageBase = p13n(localize(i18next)(PageView));
49
+ let UserListPage = class UserListPage extends UserListPageBase {
50
+ constructor() {
51
+ super(...arguments);
52
+ /*
53
+ * 카드가 아니라 **행**이다. 카드는 그림으로 알아보는 물건(형상 카탈로그)의 모양이고,
54
+ * 사람은 이름으로 알아본다. 좁은 화면에서는 `LIST` 가 같은 행을 접어 그린다.
55
+ */
56
+ this.mode = isMobileDevice() ? 'LIST' : 'GRID';
57
+ this.lockDurationMinutes = 10;
58
+ /*
59
+ * **읽는 중 · 못 읽었다 · 읽었는데 없다 를 가른다.** 셋을 한 문장으로 그리면 못 읽은 것을
60
+ * 「없다」로 읽고, 사용자가 이미 있는 것을 만들러 간다.
61
+ */
62
+ this.fetchState = 'loading';
63
+ this.total = 0;
64
+ this.filtered = false;
65
+ }
66
+ static { this.styles = [
67
+ ScrollbarStyles,
68
+ CommonGristStyles,
69
+ CommonHeaderStyles,
70
+ css `
71
+ :host {
72
+ display: flex;
73
+ flex-direction: column;
74
+ width: 100%;
75
+ background-color: var(--md-sys-color-surface);
76
+ color: var(--md-sys-color-on-surface);
77
+ /* 떠 있는 단추의 기준 상자 */
78
+ position: relative;
79
+ }
80
+
81
+ ox-grist {
82
+ flex: 1;
83
+ min-height: 0;
84
+ }
85
+
86
+ /*
87
+ ── 상태 배지 ──────────────────────────────────────────────────────────
88
+ 색은 **짝으로** 쓴다(container / on-container). 한쪽만 쓰면 야간에 대비가 깨진다.
89
+ 그리고 색은 「무엇이 나를 기다리나」에만 쓴다 — 활성은 기본값이라 색이 없다.
90
+ 담는 면이 행이므로 모서리는 작게 잡는다.
91
+ */
92
+ .badge {
93
+ display: inline-block;
94
+ padding: 2px 8px;
95
+ border-radius: var(--md-sys-shape-corner-small, 6px);
96
+ font-size: 0.78rem;
97
+ white-space: nowrap;
98
+ background-color: var(--md-sys-color-surface-variant);
99
+ color: var(--md-sys-color-on-surface-variant);
100
+ }
101
+ .badge[attention] {
102
+ background-color: var(--md-sys-color-error-container);
103
+ color: var(--md-sys-color-on-error-container);
104
+ }
105
+ .badge[quiet] {
106
+ color: var(--md-sys-color-outline);
107
+ }
108
+
109
+ /* 격자를 떼지 않고 위에 겹친다 — 격자가 살아 있어야 다시 읽을 수 있다. */
110
+ ox-oops-note[note] {
111
+ position: absolute;
112
+ inset: 0;
113
+ z-index: 5;
114
+ background-color: var(--md-sys-color-surface);
115
+ }
116
+
117
+ /*
118
+ ── 사람 부르기 ────────────────────────────────────────────────────────
119
+ 이 화면의 주된 동작 하나. 글자를 함께 둔다 — 더하기 표만으로는 무엇을 부르는지 모른다.
120
+ 떠 있는 것의 모서리는 알약이다(담는 면을 따르지 않는다).
121
+ */
122
+ button[fab] {
123
+ position: absolute;
124
+ right: var(--spacing-large, 12px);
125
+ bottom: var(--spacing-large, 12px);
126
+ z-index: 10;
127
+
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 6px;
131
+ padding: 10px 18px 10px 14px;
132
+ font: var(--label-font, inherit);
133
+ font-size: 0.82rem;
134
+ font-weight: 600;
135
+ color: var(--md-sys-color-on-primary-container);
136
+ background-color: var(--md-sys-color-primary-container);
137
+ border: none;
138
+ border-radius: var(--md-sys-shape-corner-full, 999px);
139
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
140
+ cursor: pointer;
141
+ }
142
+ button[fab]:hover {
143
+ color: var(--md-sys-color-on-primary, #fff);
144
+ background-color: var(--md-sys-color-primary);
145
+ }
146
+ button[fab] md-icon {
147
+ --md-icon-size: 20px;
148
+ }
149
+ `
150
+ ]; }
151
+ get context() {
152
+ return {
153
+ title: i18next.t('text.user management'),
154
+ help: 'auth/users'
155
+ };
156
+ }
157
+ render() {
158
+ return html `
159
+ <ox-grist
160
+ .mode=${this.mode}
161
+ .config=${this.config}
162
+ .fetchHandler=${this.fetchHandler.bind(this)}
163
+ .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')}
164
+ >
165
+ <div slot="headroom" class="header">
166
+ <div class="filters">
167
+ <ox-filters-form autofocus></ox-filters-form>
168
+ </div>
169
+ <ox-context-page-toolbar class="actions" .context=${this.context}></ox-context-page-toolbar>
170
+ </div>
171
+
172
+ <ox-grist-personalizer slot="setting"></ox-grist-personalizer>
173
+ </ox-grist>
174
+
175
+ ${this.renderNote()}
176
+
177
+ <button fab @click=${() => this.onInvite()}>
178
+ <md-icon>person_add</md-icon>${i18next.t('label.add user')}
179
+ </button>
180
+ `;
181
+ }
182
+ /**
183
+ * 목록 위에 겹쳐 그린다 — 격자를 떼면 다시 읽을 길이 없어진다.
184
+ *
185
+ * 거른 결과가 비어 있는 것은 **가르칠 자리가 아니다.** 그때는 격자가 스스로 「없음」을
186
+ * 말하게 두고, 여기서는 아무것도 그리지 않는다.
187
+ */
188
+ renderNote() {
189
+ if (this.fetchState === 'loading') {
190
+ return nothing;
191
+ }
192
+ if (this.fetchState === 'failed') {
193
+ return html `<ox-oops-note
194
+ note
195
+ icon="error_outline"
196
+ title=${i18next.t('text.failed to fetch')}
197
+ description=${i18next.t('text.please try again')}
198
+ ></ox-oops-note>`;
199
+ }
200
+ if (this.total === 0 && !this.filtered) {
201
+ /* 새 도메인의 첫 화면. 소유자는 역할 없이도 모두 열리므로 그 사실부터 말한다. */
202
+ return html `<ox-oops-note
203
+ note
204
+ icon="group_add"
205
+ title=${i18next.t('text.no other user yet')}
206
+ description=${i18next.t('text.owner opens everything without a role')}
207
+ ></ox-oops-note>`;
208
+ }
209
+ return nothing;
210
+ }
211
+ async pageInitialized() {
212
+ this.config = {
213
+ /*
214
+ * 카드에 실을 것. 표 모드도 같은 컬럼 설정을 쓰므로 **두 모드가 다른 말을 하지 않는다.**
215
+ */
216
+ list: {
217
+ fields: ['name', 'email'],
218
+ details: ['status', 'roles']
219
+ },
220
+ columns: [
221
+ { type: 'gutter', gutterName: 'sequence', fixed: true },
222
+ {
223
+ type: 'string',
224
+ name: 'name',
225
+ header: i18next.t('field.name'),
226
+ record: { align: 'left', renderer: nameWithOwnerMark },
227
+ filter: 'search',
228
+ sortable: true,
229
+ width: 160
230
+ },
231
+ {
232
+ type: 'string',
233
+ name: 'email',
234
+ header: i18next.t('field.email'),
235
+ record: { align: 'left' },
236
+ filter: 'search',
237
+ sortable: true,
238
+ width: 220
239
+ },
240
+ {
241
+ /*
242
+ * 상태 하나로 두 가지를 말한다 — 지금 상태와, 잠긴 경우 **언제 풀리는지**.
243
+ * `lockedAt` 과 설정된 잠금 시간으로 계산한다. 옛 화면은 「잠김」까지만 말했고,
244
+ * 그래서 관리자가 기다리면 되는지 풀어 줘야 하는지 알 수 없었다.
245
+ */
246
+ type: 'string',
247
+ name: 'status',
248
+ header: i18next.t('field.status'),
249
+ record: { align: 'center', renderer: statusRenderer(() => this.lockDurationMinutes) },
250
+ filter: {
251
+ options: ['', 'activated', 'locked', 'inactive'],
252
+ multiple: false,
253
+ operator: 'eq'
254
+ },
255
+ sortable: true,
256
+ width: 140
257
+ },
258
+ {
259
+ /*
260
+ * 역할이 비어 있는 것을 **눈에 띄게** 그린다. 역할 없는 활성 계정은 로그인은 되는데
261
+ * 화면이 하나도 없다 — 옛 화면에서는 그것이 빈 칸이라 아무도 알아채지 못했다.
262
+ */
263
+ type: 'string',
264
+ name: 'roles',
265
+ header: i18next.t('field.roles'),
266
+ record: { align: 'left', renderer: rolesRenderer },
267
+ width: 260
268
+ },
269
+ {
270
+ type: 'datetime',
271
+ name: 'updatedAt',
272
+ header: i18next.t('field.updated_at'),
273
+ record: { align: 'center' },
274
+ sortable: true,
275
+ width: 160
276
+ }
277
+ ],
278
+ rows: {
279
+ selectable: { multiple: false },
280
+ handlers: {
281
+ click: (_columns, _data, _column, record) => this.onSelectUser(record)
282
+ }
283
+ },
284
+ sorters: [{ name: 'name' }]
285
+ };
286
+ }
287
+ async pageUpdated(changes, lifecycle) {
288
+ if (this.active) {
289
+ await this.updateComplete;
290
+ this.grist.fetch();
291
+ }
292
+ }
293
+ /**
294
+ * 사람만 가져온다. **거르는 것은 서버가 한다** — 옛 화면은 전부 받아 클라이언트에서 나눴다.
295
+ *
296
+ * `userType` 은 `varchar` 라 값이 넷 밖으로 나갈 수 있다. 그래서 「기계가 아닌 것」을
297
+ * `not-in` 으로 요구한다 — 「사람인 것」을 `in` 으로 요구하면 값이 비어 있는 사람이 사라진다.
298
+ * (`userType` 을 enum 으로 못 박는 것은 §3 의 일이다.)
299
+ */
300
+ async fetchHandler({ filters = [], page, limit, sortings = [] }) {
301
+ this.fetchState = 'loading';
302
+ /* 사람만 보려고 이 화면이 얹는 조건은 「거른 것」으로 세지 않는다 — 그것까지 세면
303
+ 아무도 없는 도메인이 「거른 결과가 없다」로 보여 첫 안내가 안 나온다. */
304
+ this.filtered = filters.length > 0;
305
+ try {
306
+ const response = (await client.query({
307
+ query: gql `
308
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
309
+ responses: users(filters: $filters, pagination: $pagination, sortings: $sortings) {
310
+ items {
311
+ id
312
+ username
313
+ name
314
+ email
315
+ userType
316
+ status
317
+ lockedAt
318
+ owner
319
+ roles {
320
+ id
321
+ name
322
+ }
323
+ updatedAt
324
+ }
325
+ total
326
+ }
327
+ }
328
+ `,
329
+ variables: {
330
+ filters: [
331
+ ...filters,
332
+ {
333
+ name: 'userType',
334
+ operator: 'notin',
335
+ value: ['application', 'appliance']
336
+ }
337
+ ],
338
+ pagination: { page, limit },
339
+ sortings: sortings.length ? sortings : [{ name: 'name' }]
340
+ }
341
+ })).data.responses;
342
+ this.total = response.total;
343
+ this.fetchState = 'loaded';
344
+ return {
345
+ records: response.items,
346
+ total: response.total
347
+ };
348
+ }
349
+ catch (e) {
350
+ /* 못 읽은 것을 「없다」로 그리지 않는다. */
351
+ this.fetchState = 'failed';
352
+ throw e;
353
+ }
354
+ }
355
+ /**
356
+ * 고른 사람의 역할을 고치는 자리를 연다.
357
+ *
358
+ * `domain-management` 가 격자 행에서 `ox-attribute-view` 를 여는 것과 같은 모양이다 —
359
+ * `handlers.click` 에서 `openPopup` 에 template 을 넘긴다.
360
+ *
361
+ * `escapable` 은 넘기지 않는다. 이 앱의 기본값이 `false` 이고, 그것이 곧 **ESC 에 먼저
362
+ * 묻는다**는 뜻이다 — 편집면에서 원하는 거동이 기본값이다.
363
+ */
364
+ onSelectUser(record) {
365
+ const popup = openPopup(html `
366
+ <user-detail
367
+ .user=${record}
368
+ @close=${() => popup.close()}
369
+ @user-updated=${() => {
370
+ popup.close();
371
+ /* 고친 사람만 다시 읽을 길이 없다 — 격자가 가진 것은 쪽 단위 새로고침뿐이다. */
372
+ this.grist.fetch();
373
+ }}
374
+ ></user-detail>
375
+ `, {
376
+ backdrop: true,
377
+ size: 'large',
378
+ title: record?.name || record?.username || record?.email
379
+ });
380
+ }
381
+ /** 목록의 역할 편집과 같은 모양으로 연다 — `openPopup` 에 template 을 넘긴다. */
382
+ onInvite() {
383
+ const popup = openPopup(html `
384
+ <user-invite
385
+ @close=${() => popup.close()}
386
+ @user-invited=${() => {
387
+ popup.close();
388
+ this.grist.fetch();
389
+ }}
390
+ ></user-invite>
391
+ `, {
392
+ backdrop: true,
393
+ size: 'large',
394
+ title: i18next.t('label.add user')
395
+ });
396
+ }
397
+ };
398
+ __decorate([
399
+ property({ type: Object }),
400
+ __metadata("design:type", Object)
401
+ ], UserListPage.prototype, "config", void 0);
402
+ __decorate([
403
+ property({ type: String }),
404
+ __metadata("design:type", String)
405
+ ], UserListPage.prototype, "mode", void 0);
406
+ __decorate([
407
+ state(),
408
+ __metadata("design:type", Number)
409
+ ], UserListPage.prototype, "lockDurationMinutes", void 0);
410
+ __decorate([
411
+ state(),
412
+ __metadata("design:type", String)
413
+ ], UserListPage.prototype, "fetchState", void 0);
414
+ __decorate([
415
+ state(),
416
+ __metadata("design:type", Number)
417
+ ], UserListPage.prototype, "total", void 0);
418
+ __decorate([
419
+ state(),
420
+ __metadata("design:type", Boolean)
421
+ ], UserListPage.prototype, "filtered", void 0);
422
+ __decorate([
423
+ query('ox-grist'),
424
+ __metadata("design:type", Function)
425
+ ], UserListPage.prototype, "grist", void 0);
426
+ UserListPage = __decorate([
427
+ customElement('user-list-page')
428
+ ], UserListPage);
429
+ export { UserListPage };
430
+ /** 소유자는 이름 옆에 표시한다. 서버가 사람마다 `owner` 를 채워 준다(공동 소유자도 함께). */
431
+ function nameWithOwnerMark(value, _column, record) {
432
+ return record?.owner
433
+ ? html `<md-icon style="--md-icon-size: 16px; vertical-align: -3px;">supervisor_account</md-icon> ${value}`
434
+ : html `${value}`;
435
+ }
436
+ /**
437
+ * 잠긴 계정이 **언제 풀리는지**까지 말한다.
438
+ *
439
+ * 판정은 서버(`account-lock-rule.ts`)가 하고 여기서는 남은 시간만 보여 준다 —
440
+ * 화면이 판정을 다시 하면 두 곳이 다른 답을 낸다. `lockedAt` 이 비어 있으면 자동 해제가
441
+ * 없다는 뜻이므로 남은 시간을 말하지 않는다.
442
+ */
443
+ function statusRenderer(durationMinutes) {
444
+ return (value, _column, record) => {
445
+ if (value === 'locked') {
446
+ return html `<span class="badge" attention
447
+ >${i18next.t('label.locked-user')}${lockLeft(record, durationMinutes())}</span
448
+ >`;
449
+ }
450
+ if (value === 'inactive') {
451
+ return html `<span class="badge" quiet>${i18next.t('label.inactive-user')}</span>`;
452
+ }
453
+ /* 활성은 기본값이다 — 색을 쓰지 않는다. 색은 「무엇이 나를 기다리나」에만 쓴다. */
454
+ return html `<span class="badge">${i18next.t('label.activated-user', { defaultValue: value })}</span>`;
455
+ };
456
+ }
457
+ /** 잠금이 언제 풀리는지. 판정은 서버가 하고 여기서는 남은 시간만 말한다. */
458
+ function lockLeft(record, minutes) {
459
+ const lockedAt = record?.lockedAt ? new Date(record.lockedAt).getTime() : NaN;
460
+ if (Number.isNaN(lockedAt) || !(minutes > 0)) {
461
+ /* `lockedAt` 이 없으면 자동 해제가 없다는 뜻이다 — 남은 시간을 지어내지 않는다. */
462
+ return nothing;
463
+ }
464
+ const left = Math.ceil((lockedAt + minutes * 60 * 1000 - Date.now()) / 60000);
465
+ return left > 0 ? html ` · ${i18next.t('text.unlocks in x minutes', { x: left })}` : nothing;
466
+ }
467
+ /** 역할이 없는 것은 빈 칸이 아니라 **문장**으로 말한다. */
468
+ function rolesRenderer(_value, _column, record) {
469
+ const roles = record?.roles || [];
470
+ return roles.length
471
+ ? html `${roles.map((role) => role.name).join(' · ')}`
472
+ : html `<span class="badge" attention>${i18next.t('text.no role assigned')}</span>`;
473
+ }
474
+ //# sourceMappingURL=user-list-page.js.map