@things-factory/auth-ui 10.1.250 → 10.1.254

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.
@@ -4,7 +4,7 @@ import { html, nothing } from 'lit';
4
4
  import { browserSupportsWebAuthnAutofill, startAuthentication } from '@simplewebauthn/browser';
5
5
  import { i18next } from '@operato/i18n';
6
6
  import { notify } from '@operato/layout';
7
- import { readSigninAnswer, wasCancelled } from '../utils/passkey.js';
7
+ import { passkeySigninForm, wasCancelled } from '../utils/passkey.js';
8
8
  import { AbstractAuthPage } from './abstract-auth-page.js';
9
9
  const isAvailableWebauthn = 'PublicKeyCredential' in window;
10
10
  export class AbstractSign extends AbstractAuthPage {
@@ -88,16 +88,9 @@ export class AbstractSign extends AbstractAuthPage {
88
88
  /* The field lives in md-filled-text-field's shadow root, where a document query does not reach. */
89
89
  verifyBrowserAutofillInput: false
90
90
  });
91
- const answer = await readSigninAnswer(await fetch('/auth/signin-webauthn', {
92
- method: 'POST',
93
- headers: { 'Content-Type': 'application/json' },
94
- body: JSON.stringify({ response, redirectTo: this.redirectTo || '/' })
95
- }));
96
- if (answer.outcome === 'signed-in') {
97
- window.location.href = answer.redirectURL;
98
- return;
99
- }
100
- notify({ level: 'error', message: answer.message || i18next.t('error.authn verification failed') });
91
+ const form = passkeySigninForm(document, response, this.redirectTo || '/');
92
+ this.renderRoot.appendChild(form);
93
+ form.submit();
101
94
  }
102
95
  catch (error) {
103
96
  /* A closed prompt, or the autofill request cancelled by the button - the person knows. */
@@ -1 +1 @@
1
- {"version":3,"file":"abstract-sign.js","sourceRoot":"","sources":["../../client/components/abstract-sign.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/outlined-button.js'\nimport '@operato/i18n/ox-i18n.js'\n\nimport { html, nothing } from 'lit'\nimport { browserSupportsWebAuthnAutofill, startAuthentication } from '@simplewebauthn/browser'\n\nimport { i18next } from '@operato/i18n'\nimport { notify } from '@operato/layout'\n\nimport { readSigninAnswer, wasCancelled } from '../utils/passkey.js'\nimport { AbstractAuthPage } from './abstract-auth-page.js'\n\nconst isAvailableWebauthn = 'PublicKeyCredential' in window\n\nexport abstract class AbstractSign extends AbstractAuthPage {\n async submit() {\n this.formEl.submit()\n }\n\n get formfields() {\n const username = this.data?.username || ''\n const autocompletable = this.autocompletable\n\n return html`\n <input id=\"redirectTo\" type=\"hidden\" name=\"redirectTo\" .value=${this.redirectTo || '/'} />\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"username\"\n type=\"text\"\n label=${String(i18next.t('field.user-id or email'))}\n required\n .value=${username}\n autocomplete=${autocompletable ? 'username webauthn' : 'off'}\n autocapitalize=\"off\"\n pattern=\"^(?:[A-Za-z0-9]*|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,})$\"\n @input=${(e: Event) => {\n const target = e.target as HTMLInputElement\n if (target.validity.typeMismatch) {\n target.setCustomValidity(i18next.t('text.invalid-username'))\n } else {\n target.setCustomValidity('')\n }\n }}\n ><md-icon slot=\"leading-icon\">id_card</md-icon></md-filled-text-field\n >\n </div>\n <div class=\"field\">\n <md-filled-text-field\n name=\"password\"\n type=\"password\"\n label=${String(i18next.t('field.password'))}\n autocomplete=${autocompletable ? 'current-password' : 'off'}\n required\n ><md-icon slot=\"leading-icon\">password</md-icon></md-filled-text-field\n >\n </div>\n\n <div class=\"submit-buttons-container\">\n <md-elevated-button class=\"submit-button\" type=\"button\" raised @click=${e => this._onSubmit(e)}>\n <ox-i18n msgid=\"field.${this.pageName}\"> </ox-i18n>\n </md-elevated-button>\n ${isAvailableWebauthn && autocompletable\n ? html`<md-outlined-button class=\"passkey\" type=\"button\" @click=${() => this.signinWithPasskey()}>\n <md-icon slot=\"icon\">passkey</md-icon>${i18next.t('button.signin with passkey')}\n </md-outlined-button>`\n : nothing}\n </div>\n `\n }\n\n firstUpdated() {\n super.firstUpdated()\n\n /*\n * Where the browser can, the passkeys it holds for this site show in the user-id field's\n * suggestions (ADR-0156 decision 5). The request waits for the person; pressing the passkey\n * button cancels it and asks again.\n */\n if (isAvailableWebauthn && this.autocompletable) {\n this.signinWithPasskey({ autofill: true })\n }\n }\n\n /**\n * Sign in with a passkey - one on this device, or a phone nearby through the QR code the browser\n * shows (ADR-0156 decision 3).\n */\n async signinWithPasskey({ autofill = false }: { autofill?: boolean } = {}) {\n try {\n if (autofill && !(await browserSupportsWebAuthnAutofill())) return\n\n const optionsJSON = await fetch('/auth/signin-webauthn/challenge').then(res => res.json())\n const response = await startAuthentication({\n optionsJSON,\n useBrowserAutofill: autofill,\n /* The field lives in md-filled-text-field's shadow root, where a document query does not reach. */\n verifyBrowserAutofillInput: false\n })\n\n const answer = await readSigninAnswer(\n await fetch('/auth/signin-webauthn', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ response, redirectTo: this.redirectTo || '/' })\n })\n )\n\n if (answer.outcome === 'signed-in') {\n window.location.href = answer.redirectURL\n return\n }\n\n notify({ level: 'error', message: answer.message || i18next.t('error.authn verification failed') })\n } catch (error) {\n /* A closed prompt, or the autofill request cancelled by the button - the person knows. */\n if (wasCancelled(error)) return\n\n console.error('passkey sign-in failed:', error)\n if (!autofill) {\n notify({ level: 'error', message: i18next.t('error.authn verification failed') })\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"abstract-sign.js","sourceRoot":"","sources":["../../client/components/abstract-sign.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/outlined-button.js'\nimport '@operato/i18n/ox-i18n.js'\n\nimport { html, nothing } from 'lit'\nimport { browserSupportsWebAuthnAutofill, startAuthentication } from '@simplewebauthn/browser'\n\nimport { i18next } from '@operato/i18n'\nimport { notify } from '@operato/layout'\n\nimport { passkeySigninForm, wasCancelled } from '../utils/passkey.js'\nimport { AbstractAuthPage } from './abstract-auth-page.js'\n\nconst isAvailableWebauthn = 'PublicKeyCredential' in window\n\nexport abstract class AbstractSign extends AbstractAuthPage {\n async submit() {\n this.formEl.submit()\n }\n\n get formfields() {\n const username = this.data?.username || ''\n const autocompletable = this.autocompletable\n\n return html`\n <input id=\"redirectTo\" type=\"hidden\" name=\"redirectTo\" .value=${this.redirectTo || '/'} />\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"username\"\n type=\"text\"\n label=${String(i18next.t('field.user-id or email'))}\n required\n .value=${username}\n autocomplete=${autocompletable ? 'username webauthn' : 'off'}\n autocapitalize=\"off\"\n pattern=\"^(?:[A-Za-z0-9]*|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,})$\"\n @input=${(e: Event) => {\n const target = e.target as HTMLInputElement\n if (target.validity.typeMismatch) {\n target.setCustomValidity(i18next.t('text.invalid-username'))\n } else {\n target.setCustomValidity('')\n }\n }}\n ><md-icon slot=\"leading-icon\">id_card</md-icon></md-filled-text-field\n >\n </div>\n <div class=\"field\">\n <md-filled-text-field\n name=\"password\"\n type=\"password\"\n label=${String(i18next.t('field.password'))}\n autocomplete=${autocompletable ? 'current-password' : 'off'}\n required\n ><md-icon slot=\"leading-icon\">password</md-icon></md-filled-text-field\n >\n </div>\n\n <div class=\"submit-buttons-container\">\n <md-elevated-button class=\"submit-button\" type=\"button\" raised @click=${e => this._onSubmit(e)}>\n <ox-i18n msgid=\"field.${this.pageName}\"> </ox-i18n>\n </md-elevated-button>\n ${isAvailableWebauthn && autocompletable\n ? html`<md-outlined-button class=\"passkey\" type=\"button\" @click=${() => this.signinWithPasskey()}>\n <md-icon slot=\"icon\">passkey</md-icon>${i18next.t('button.signin with passkey')}\n </md-outlined-button>`\n : nothing}\n </div>\n `\n }\n\n firstUpdated() {\n super.firstUpdated()\n\n /*\n * Where the browser can, the passkeys it holds for this site show in the user-id field's\n * suggestions (ADR-0156 decision 5). The request waits for the person; pressing the passkey\n * button cancels it and asks again.\n */\n if (isAvailableWebauthn && this.autocompletable) {\n this.signinWithPasskey({ autofill: true })\n }\n }\n\n /**\n * Sign in with a passkey - one on this device, or a phone nearby through the QR code the browser\n * shows (ADR-0156 decision 3).\n */\n async signinWithPasskey({ autofill = false }: { autofill?: boolean } = {}) {\n try {\n if (autofill && !(await browserSupportsWebAuthnAutofill())) return\n\n const optionsJSON = await fetch('/auth/signin-webauthn/challenge').then(res => res.json())\n const response = await startAuthentication({\n optionsJSON,\n useBrowserAutofill: autofill,\n /* The field lives in md-filled-text-field's shadow root, where a document query does not reach. */\n verifyBrowserAutofillInput: false\n })\n\n const form = passkeySigninForm(document, response, this.redirectTo || '/')\n this.renderRoot.appendChild(form)\n form.submit()\n } catch (error) {\n /* A closed prompt, or the autofill request cancelled by the button - the person knows. */\n if (wasCancelled(error)) return\n\n console.error('passkey sign-in failed:', error)\n if (!autofill) {\n notify({ level: 'error', message: i18next.t('error.authn verification failed') })\n }\n }\n }\n}\n"]}
@@ -131,14 +131,25 @@ let UserDetail = class UserDetail extends localize(i18next)(LitElement) {
131
131
  gap: 2px;
132
132
  }
133
133
 
134
+ /*
135
+ A role is its name with what it opens under it. Side by side, a long description squeezed the name a
136
+ syllable a line on a phone (「직/원」, 2026-11-06).
137
+ */
134
138
  ul[roles] li {
135
- display: flex;
136
- align-items: center;
137
- gap: var(--spacing-medium, 8px);
139
+ display: grid;
140
+ grid-template-columns: auto minmax(0, 1fr);
141
+ column-gap: var(--spacing-medium, 8px);
142
+ align-items: baseline;
138
143
  padding: 3px 0;
139
144
  }
140
145
 
146
+ ul[roles] li input {
147
+ grid-row: span 2;
148
+ align-self: start;
149
+ }
150
+
141
151
  ul[roles] li [desc] {
152
+ grid-column: 2;
142
153
  color: var(--ui-muted);
143
154
  font-size: var(--ui-t-small, 11.5px);
144
155
  }
@@ -1 +1 @@
1
- {"version":3,"file":"user-detail.js","sourceRoot":"","sources":["../../client/components/user-detail.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/outlined-button.js'\nimport '@operato/popup/ox-popup-actions.js'\n\nimport './ownership-transfer-popup.js'\nimport { statusRenderer } from './user-status-renderer.js'\nimport { openPasswordSetupLink } from './password-setup-link-popup.js'\n\nimport gql from 'graphql-tag'\nimport { lockedParentRoles } from '../utils/role-origin.js'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport type { CSSResultGroup } from 'lit'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { InheritedValueType, store } from '@operato/shell'\nimport { CommonPopupStyles, FormFieldStyles, PopupBodyStyles } from '@operato/styles'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\n/**\n * 한 사람의 역할을 고치는 자리.\n *\n * ── 담는 그릇을 모른다 ──────────────────────────────────────────────────────\n * 팝업이든 서랍이든 그 안에 들어간다. 그래서 여기서는 **아무것도 열거나 닫지 않고** 두 가지만\n * 알린다 — 저장이 끝났다(`user-updated`), 닫아 달라(`close`). 그릇이 어느 것이 될지는 아직\n * 정해지지 않았고, 그 결정이 이 파일을 바꾸지 않게 한다.\n *\n * ── 옛 화면이 하던 것 (`components/user-role-editor.ts`) ────────────────────\n * 저장할 때 `querySelectorAll('role-selector')` 로 **화면을 훑어** 「가능한 역할 전부」와\n * 「고른 역할」을 만들어 보냈다.\n *\n * `updateUserRoles` 의 계약은 이렇다 — `availableRoles` 는 **부르는 쪽이 책임지는 범위**이고,\n * 그 범위 안의 역할을 일단 다 뗀 다음 `selectedRoles` 를 다시 붙인다. 범위 밖의 역할은\n * 건드리지 않는다(`user-mutation.ts` 의 `updateUserRoles`).\n *\n * 계약 자체는 옳다 — 화면이 일부만 편집할 수 있게 해 준다. 잘못된 것은 **그 범위를 DOM 에서\n * 만든 것**이다. 목록이 늦게 오거나 잘려 있으면 화면은 그것을 모른 채 좁은 범위를 보내고,\n * 안 보낸 역할이 어떻게 되는지가 그날의 렌더링에 달린다.\n *\n * 여기서는 범위를 **상태에서** 만든다. 읽어 온 목록이 곧 범위이고, 아직 못 읽었으면 저장하지\n * 않는다.\n */\n@customElement('user-detail')\nexport class UserDetail extends localize(i18next)(LitElement) {\n /*\n * 팝업의 집 모양을 그대로 쓴다(page-standard §7·§8) — 본문만 스크롤하고 단추 줄은 `ox-popup-actions`\n * 하나다. 제목은 이 팝업이 무엇인지(사용자 상세 정보)를 말하고, 누구인지는 대상 띠(`.what`)가 말한다.\n */\n static styles: CSSResultGroup = [\n CommonPopupStyles,\n PopupBodyStyles,\n FormFieldStyles,\n css`\n /* 상태 한 줄 — 그 줄에 대한 동작은 줄 끝에 둔다(§8 「줄 하나」). */\n [state-line] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n }\n\n [state-line] [label],\n [group-label] {\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n /* 팝업 안의 컨트롤은 담는 카드를 따라 알약이다 — 공용 규칙은 단추 줄 안에만 걸린다. */\n [state-line] md-outlined-button {\n margin-left: auto;\n --md-outlined-button-container-height: var(--popup-action-height, 32px);\n --md-outlined-button-container-shape: var(--popup-control-radius);\n }\n\n [group-label] {\n margin-top: var(--spacing-medium, 8px);\n }\n\n ul[roles] {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n ul[roles] li {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: 3px 0;\n }\n\n ul[roles] li [desc] {\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n .note md-icon {\n --md-icon-size: 16px;\n vertical-align: -3px;\n }\n `\n ]\n\n /** 목록이 골라 준 사람. `id` 와 `username`(없으면 `email`)이 있어야 한다. */\n @property({ type: Object }) user: any\n\n @state() private availableRoles: any[] = []\n @state() private selected: Set<string> = new Set()\n @state() private loading: boolean = true\n @state() private saving: boolean = false\n\n /*\n * **내가 이 도메인에서 무엇을 할 수 있나 — 서버 선언을 그대로 되묻는다.**\n *\n * `/auth/profile` 이 `privileges`·`owner`·`super` 를 전부 담아 주고, `hasPrivilege`\n * (`auth-base/client/profiled.ts`)가 서버의 `checkPermission` 과 같은 규칙으로 그것을 판정한다\n * — 안전하지 않은 IP 에서 소유권 우회를 빼는 것까지 같다. 그래서 **서버 지시자에 적힌 인자를\n * 그대로 넘기는 것**이 화면과 문을 어긋나지 않게 하는 방법이다.\n *\n * 옛 화면은 이것을 두고 `me.email === domainOwner.email` 로 판정했다 — 목록에서 뽑은 대표\n * 소유자 한 명과 견주는 것이라 **공동 소유자가 자기 권한을 잃었고**, `user:mutation` 을 역할로\n * 받은 사람은 아예 단추를 못 봤다.\n *\n * 둘이 다른 선언을 쓴다.\n *\n * 활성/비활성·초기화·제외 user:mutation + domainOwnerGranted\n * 소유권 이전 domainOwnerGranted + superUserGranted (권한 쌍이 없다)\n *\n * ⚠ 수퍼유저가 자동으로 다 통과하지 않는다 — `checkPermission` 은 지시자가\n * `superUserGranted` 를 선언한 곳에서만 그것을 본다. 그래서 위 둘을 뭉치면 안 된다.\n */\n @state() private canManage: boolean = false\n @state() private canOwn: boolean = false\n @state() private myEmail: string = ''\n\n /**\n * 이 사람에게 이 도메인이 비밀번호 설정 링크를 발급해도 되는가 — 서버 규칙(`password-reset-rule.ts`)의 답.\n *\n * 비밀번호는 계정 하나에 하나다. 다른 도메인에서도 일하는 사람, 어느 도메인이든 오너인 사람\n * (수퍼유저 포함)의 비밀번호는 이 도메인이 거두지 않는다(ADR-0161 결정 4).\n */\n @state() private setupLinkIssuable: boolean = false\n\n /**\n * 이 도메인(과 부모)의 역할 id.\n *\n * 소유권 이전은 **받는 사람이 이 도메인에 역할을 하나라도 가질 때만** 된다\n * (`transferOwner` 가 상속 역할까지 자격으로 인정하고, 그 밖은 던진다). 파트너십으로 넘어온\n * 역할은 자격이 아니므로 `availableRoles`(합친 것)로는 판정할 수 없다.\n */\n @state() private domainRoleIds: Set<string> = new Set()\n /** The parent domain's roles this person may not change here - locked, with the reason under the list. */\n @state() private locked: Set<string> = new Set()\n private unsubscribe?: () => void\n\n /*\n * **저장할 수 있나 — 단추와 처리기가 같은 답을 본다.**\n *\n * 셋을 갈라야 한다. 화면에서 갈라 놓고 저장에서 뭉치면, 「아직 못 읽었다」와 「읽었는데\n * 역할이 없다」가 똑같이 **눌러도 아무 일이 없는 단추**가 된다. 그것이 조용히 되돌아가는\n * 자리이고, 사람은 저장이 됐는지 안 됐는지 알 수 없다.\n *\n * 못 읽었다 아직 범위를 모른다 — 눌리면 안 된다\n * 역할이 0개 보낼 것이 없다 — 눌리면 안 되고, 무엇을 해야 하는지는 목록 자리가 말한다\n * 읽었고 있다 저장한다\n */\n private get savable(): boolean {\n return !this.loading && !this.saving && this.availableRoles.length > 0\n }\n\n render() {\n const user = this.user || {}\n\n return html`\n <div class=\"body\">\n <div class=\"what\">${user.name || user.username || user.email} · ${user.email}</div>\n\n ${\n user.owner\n ? html`<div class=\"note\">\n <md-icon>supervisor_account</md-icon>\n ${i18next.t('text.owner opens everything without a role')}\n </div>`\n : nothing\n }\n ${this.renderAccountState(user)}\n\n <div group-label>${i18next.t('field.roles')}</div>\n ${this.renderRoles()}\n </div>\n\n <ox-popup-actions>\n ${this.renderIrreversible(user)}\n <md-text-button slot=\"cancel\" @click=${() => this.dispatchEvent(new CustomEvent('close'))}>\n ${i18next.t('button.cancel')}\n </md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${!this.savable} @click=${() => this.onSave()}>\n ${i18next.t('button.save')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n /**\n * 되돌릴 수 없는 조치 — 단추 줄의 왼쪽 끝, 빨간 글자(page-standard §8, `ox-popup-actions` 의 `start`).\n *\n * 옛 화면은 저장·소유권 이전·초기화·제외·비활성화를 **한 줄에 나란히** 뒀다. 매일 누르는\n * 「저장」 옆에 한 번 누르면 끝인 「제외」가 같은 크기로 있었다. 단추 줄이 그 거리를 맡는다 —\n * 저장은 오른쪽 끝, 이것들은 왼쪽 끝이고, 색은 글자에만 쓴다.\n *\n * ── 무엇을 언제 내주나 ──────────────────────────────────────────────────────\n * 조건은 전부 **서버가 실제로 던지는 것**을 그대로 옮긴 것이다. 눌리는 단추를 보여 주고\n * 서버가 거절하면 그것은 안내가 아니라 오류다.\n *\n * 비밀번호 설정 링크 서버가 된다고 하고(`mayIssuePasswordSetupLink`), 내가 아니다\n * 소유권 이전 활성 상태고, 소유자가 아니고, 이 도메인에 역할이 하나라도 있다\n * 도메인에서 제외 소유자가 아니고, 내가 아니다\n *\n * ── 이름 ────────────────────────────────────────────────────────────────────\n * `deleteDomainUser` 는 계정을 지우지 않는다 — **이 도메인에서만 뗀다**(서버 설명도 그렇게\n * 적혀 있다). 옛 화면의 「사용자 삭제」는 하지 않는 일을 말하고 있었다.\n */\n private renderIrreversible(user: any) {\n const me = this.myEmail && user.email === this.myEmail\n\n const resettable = this.canManage && this.setupLinkIssuable && !me\n const transferable =\n this.canOwn &&\n !user.owner &&\n user.status === 'activated' &&\n (user.roles || []).some((role: any) => this.domainRoleIds.has(role.id))\n const removable = this.canManage && !user.owner && !me\n\n return html`\n ${\n resettable\n ? html`<md-text-button slot=\"start\" destructive @click=${() => this.onPasswordSetup()}>\n ${user.status === 'password_reset_required'\n ? i18next.t('button.reissue setup link')\n : i18next.t('button.password reset link')}\n </md-text-button>`\n : nothing\n }\n ${\n transferable\n ? html`<md-text-button slot=\"start\" destructive @click=${() => this.onTransfer()}>\n ${i18next.t('button.transfer owner')}\n </md-text-button>`\n : nothing\n }\n ${\n removable\n ? html`<md-text-button slot=\"start\" destructive @click=${() => this.onRemoveFromDomain()}>\n ${i18next.t('button.remove from domain')}\n </md-text-button>`\n : nothing\n }\n `\n }\n\n /**\n * 계정 상태 한 줄과, 그것을 바꾸는 단추를 그 줄 끝에(page-standard §8 「줄 하나」).\n *\n * **잠금 해제와 활성화는 서버에서 같은 것이다** — `activateUser` 가 `failCount` 를 0 으로\n * 두고 `ACTIVATED` 로 되돌린다. 그래서 부르는 것은 하나이고, **글자만 상황에 맞춰 바꾼다.**\n * 잠긴 사람에게 「활성화」라고 하면 무슨 일이 일어나는지 알 수 없다.\n *\n * 비활성화는 소유자와 나 자신에게는 내주지 않는다. 서버도 같은 것을 막지만\n * (`inactivateUser` 가 소유자·admin 을 거절한다), **화면이 눌리는 단추를 보여 주고 서버가\n * 거절하면 그것은 안내가 아니라 오류다.** 되돌릴 수 있으므로 빨간 글자가 아니다.\n */\n private renderAccountState(user: any) {\n if (!this.canManage) {\n return nothing\n }\n\n return html`\n <div state-line>\n <span label>${i18next.t('field.status')}</span>\n ${statusRenderer(() => 0)(user.status, null, user)} ${this.renderStateAction(user)}\n </div>\n `\n }\n\n private renderStateAction(user: any) {\n const status = user.status\n\n if (status === 'locked') {\n return html`<md-outlined-button @click=${() => this.onActivate()}>${i18next.t('button.unlock')}</md-outlined-button>`\n }\n\n /* 설정 대기는 활성화로 열리지 않는다 — 본인이 설정 링크로 연다(단추 줄의 설정 링크). */\n if (status === 'password_reset_required') {\n return nothing\n }\n\n if (status !== 'activated') {\n return html`<md-outlined-button @click=${() => this.onActivate()}>${i18next.t('button.activate')}</md-outlined-button>`\n }\n\n /* 활성 상태 — 되돌릴 수 있는 사람에게만, 그리고 자기 자신과 소유자에게는 안 준다. */\n if (user.owner || (this.myEmail && user.email === this.myEmail)) {\n return nothing\n }\n\n return html`<md-outlined-button @click=${() => this.onInactivate()}>${i18next.t('button.deactivate')}</md-outlined-button>`\n }\n\n private renderRoles() {\n if (this.loading) {\n return html`<div class=\"note\">${i18next.t('text.loading')}</div>`\n }\n\n if (!this.availableRoles.length) {\n /*\n * 「역할이 없다」와 「못 읽었다」는 다른 말이다. 여기까지 왔다는 것은 읽었다는 뜻이므로\n * 없는 것이 맞고, 그때는 무엇을 해야 하는지를 말한다. 저장을 막고 있으므로 실패 모양이다.\n */\n return html`<div class=\"failure\">${i18next.t('text.no role defined in this domain')}</div>`\n }\n\n return html`\n <ul roles>\n ${this.availableRoles.map(\n role => html`\n <li>\n <input\n type=\"checkbox\"\n id=\"role-${role.id}\"\n .checked=${this.selected.has(role.id)}\n ?disabled=${this.locked.has(role.id)}\n @change=${(e: Event) => this.onToggle(role.id, (e.target as HTMLInputElement).checked)}\n />\n <label for=\"role-${role.id}\">${role.name}</label>\n ${role.description ? html`<span desc>${role.description}</span>` : nothing}\n </li>\n `\n )}\n </ul>\n ${this.availableRoles.some(role => this.locked.has(role.id))\n ? html`<div class=\"note\">${i18next.t('text.parent roles are changed by the parent domain')}</div>`\n : nothing}\n `\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.applyMe()\n this.unsubscribe = store.subscribe(() => this.applyMe())\n this.checkGates()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.unsubscribe?.()\n }\n\n private applyMe() {\n const me = (store.getState() as any).auth?.user || {}\n this.myEmail = me.email || ''\n }\n\n /** 서버 지시자에 적힌 인자를 그대로 되묻는다. */\n private async checkGates() {\n this.canManage = Boolean(await hasPrivilege({ category: 'user', privilege: 'mutation', domainOwnerGranted: true }))\n this.canOwn = Boolean(await hasPrivilege({ domainOwnerGranted: true, superUserGranted: true }))\n }\n\n updated(changes: Map<string, unknown>) {\n if (changes.has('user')) {\n this.fetch()\n this.fetchSetupLinkIssuable()\n }\n }\n\n /** 관리 권한이 없으면 서버가 이 질의를 거절한다 — 그때도 단추는 없다. */\n private async fetchSetupLinkIssuable() {\n const id = this.user?.id\n this.setupLinkIssuable = false\n if (!id) {\n return\n }\n\n try {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n mayIssuePasswordSetupLink(id: $id)\n }\n `,\n variables: { id },\n context: gqlContext()\n })\n\n this.setupLinkIssuable = this.user?.id === id && response.data?.mayIssuePasswordSetupLink === true\n } catch (e) {\n this.setupLinkIssuable = false\n }\n }\n\n /**\n * 부여할 수 있는 역할을 읽는다.\n *\n * **이 사람이 지금 가진 역할은 다시 묻지 않는다** — 목록이 이미 실어 왔다(`user.roles`).\n * 옛 화면은 email 로 사용자를 다시 찾았는데, 역할 이름과 달리 email 은 유일하긴 해도\n * 손에 든 `id` 를 두고 다른 열쇠로 같은 사람을 찾는 것이었다. 안 물으면 그 문제가 없다.\n *\n * **이 도메인의 역할과 파트너십으로 넘어온 역할을 한 목록으로 합친다.** 옛 화면은 그 둘을\n * 따로 그리고 저장할 때 화면에서 다시 모았는데, 합치는 자리가 화면이면 화면이 못 그린 것은\n * 저장에서도 빠진다.\n */\n private async fetch() {\n const user = this.user\n if (!user?.id) {\n return\n }\n\n this.loading = true\n\n const response = await client.query({\n query: gql`\n query ($inherited: InheritedValueType) {\n roles(inherited: $inherited) {\n items {\n id\n name\n description\n }\n }\n grantedRoles {\n role {\n id\n name\n description\n }\n }\n parentRoles: roles(inherited: Only) {\n items {\n id\n }\n }\n mayChangeParentRoles\n }\n `,\n variables: { inherited: InheritedValueType.Include },\n context: gqlContext()\n })\n\n const roles = response.data?.roles?.items || []\n const granted = (response.data?.grantedRoles || []).map((g: any) => g.role).filter(Boolean)\n\n /* 같은 역할이 두 경로로 올 수 있다 — id 로 한 번만 담는다. */\n const byId = new Map<string, any>()\n for (const role of [...roles, ...granted]) {\n byId.set(role.id, role)\n }\n\n this.availableRoles = [...byId.values()]\n /* 소유권 이전 자격은 **이 도메인(과 부모)의 역할**만 인정한다 — 합친 목록으로는 못 본다. */\n this.domainRoleIds = new Set(roles.map((role: any) => role.id))\n this.selected = new Set((user.roles || []).map((r: any) => r.id))\n this.locked = lockedParentRoles(\n (response.data?.parentRoles?.items || []).map((role: any) => role.id),\n response.data?.mayChangeParentRoles === true\n )\n this.loading = false\n }\n\n /** 잠금 해제와 활성화가 같은 부름이다 — 서버가 `failCount` 도 0 으로 되돌린다. */\n private async onActivate() {\n await this.runUserMutation(gql`\n mutation ($username: String!) {\n activateUser(username: $username)\n }\n `)\n }\n\n private async onInactivate() {\n await this.runUserMutation(gql`\n mutation ($username: String!) {\n inactivateUser(username: $username)\n }\n `)\n }\n\n /**\n * 비밀번호 설정 링크를 발급한다 — 관리자는 비밀번호를 정하지 않는다(ADR-0161).\n *\n * 무엇이 일어나는지 · 어떻게 전할지는 그 팝업이 말하고 묻는다. 발급되면 계정이 「설정 대기」가 되므로\n * 목록을 다시 읽게 알린다(`user-updated`).\n */\n private onPasswordSetup() {\n openPasswordSetupLink(this.user, {\n onIssued: () =>\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n })\n }\n\n /**\n * 이 도메인에서만 뗀다 — 계정은 남는다.\n *\n * 그래서 확인 문구도 「지웁니다」가 아니다. 이 사람은 다른 도메인에서 계속 일할 수 있고,\n * 다시 부르면 된다.\n */\n private async onRemoveFromDomain() {\n const user = this.user || {}\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.remove from domain'),\n text: i18next.t('text.they lose access to this domain but keep the account'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const done = await this.runUserMutation(gql`\n mutation ($username: String!) {\n deleteDomainUser(username: $username)\n }\n `)\n\n if (done) {\n /* 이 사람은 이제 목록에 없다 — 상세를 열어 둘 이유가 없다. */\n this.dispatchEvent(new CustomEvent('close'))\n }\n }\n\n /**\n * 소유권을 넘긴다.\n *\n * 이미 있는 `ownership-transfer-popup` 을 그대로 쓴다 — 그 안에 **이메일을 다시 적게 하는**\n * 확인이 들어 있다. 되돌릴 수 없는 것 중에서 유일하게 두 단계인 것이고, 그 판단이 맞다.\n */\n private onTransfer() {\n const popup = openPopup(\n html`<ownership-transfer-popup\n .user=${this.user}\n @ownershipTransferred=${() => {\n popup.close()\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n }}\n ></ownership-transfer-popup>`,\n {\n backdrop: true,\n size: 'large',\n title: i18next.t('title.owner_transfer')\n }\n )\n }\n\n private async runUserMutation(mutation: any, { notify = true }: { notify?: boolean } = {}) {\n const username = this.user?.username || this.user?.email\n if (!username || this.saving) {\n return false\n }\n\n this.saving = true\n\n try {\n const response = await client.mutate({ mutation, variables: { username }, context: gqlContext() })\n\n if (response.errors) {\n return false\n }\n\n /*\n * 비밀번호 초기화만 목록을 바꾸지 않는다 — 상태도 역할도 그대로다. 그때 목록을 다시\n * 읽으면 아무것도 안 달라진 화면이 깜빡이고, 사람은 뭐가 됐는지 더 모르게 된다.\n */\n if (notify) {\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n }\n\n return true\n } finally {\n this.saving = false\n }\n }\n\n private onToggle(id: string, checked: boolean) {\n const next = new Set(this.selected)\n checked ? next.add(id) : next.delete(id)\n this.selected = next\n }\n\n /**\n * **범위를 상태에서 만든다.** 읽어 온 목록이 곧 `availableRoles` 다.\n *\n * 빈 범위를 보내면 서버는 「뗄 것이 없다」로 읽어 아무것도 안 한다 — 우연히 무해한 것이지\n * 뜻한 바가 아니므로, 그 경우는 여기까지 오지 않는다(`savable`).\n */\n private async onSave() {\n /* 단추가 이미 막고 있다 — 여기 걸리면 단추와 이 자리가 어긋난 것이다. */\n if (!this.savable) {\n return\n }\n\n const user = this.user\n const username = user?.username || user?.email\n if (!username) {\n return\n }\n\n this.saving = true\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($username: String!, $availableRoles: [ObjectRef!]!, $selectedRoles: [ObjectRef!]!) {\n updateUserRoles(username: $username, availableRoles: $availableRoles, selectedRoles: $selectedRoles) {\n id\n }\n }\n `,\n variables: {\n username,\n availableRoles: this.availableRoles.map(role => ({ id: role.id })),\n selectedRoles: this.availableRoles.filter(role => this.selected.has(role.id)).map(role => ({ id: role.id }))\n },\n context: gqlContext()\n })\n\n if (!response.errors) {\n this.dispatchEvent(new CustomEvent('user-updated', { detail: user, bubbles: true, composed: true }))\n }\n } finally {\n this.saving = false\n }\n }\n}\n"]}
1
+ {"version":3,"file":"user-detail.js","sourceRoot":"","sources":["../../client/components/user-detail.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/outlined-button.js'\nimport '@operato/popup/ox-popup-actions.js'\n\nimport './ownership-transfer-popup.js'\nimport { statusRenderer } from './user-status-renderer.js'\nimport { openPasswordSetupLink } from './password-setup-link-popup.js'\n\nimport gql from 'graphql-tag'\nimport { lockedParentRoles } from '../utils/role-origin.js'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport type { CSSResultGroup } from 'lit'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { InheritedValueType, store } from '@operato/shell'\nimport { CommonPopupStyles, FormFieldStyles, PopupBodyStyles } from '@operato/styles'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\n/**\n * 한 사람의 역할을 고치는 자리.\n *\n * ── 담는 그릇을 모른다 ──────────────────────────────────────────────────────\n * 팝업이든 서랍이든 그 안에 들어간다. 그래서 여기서는 **아무것도 열거나 닫지 않고** 두 가지만\n * 알린다 — 저장이 끝났다(`user-updated`), 닫아 달라(`close`). 그릇이 어느 것이 될지는 아직\n * 정해지지 않았고, 그 결정이 이 파일을 바꾸지 않게 한다.\n *\n * ── 옛 화면이 하던 것 (`components/user-role-editor.ts`) ────────────────────\n * 저장할 때 `querySelectorAll('role-selector')` 로 **화면을 훑어** 「가능한 역할 전부」와\n * 「고른 역할」을 만들어 보냈다.\n *\n * `updateUserRoles` 의 계약은 이렇다 — `availableRoles` 는 **부르는 쪽이 책임지는 범위**이고,\n * 그 범위 안의 역할을 일단 다 뗀 다음 `selectedRoles` 를 다시 붙인다. 범위 밖의 역할은\n * 건드리지 않는다(`user-mutation.ts` 의 `updateUserRoles`).\n *\n * 계약 자체는 옳다 — 화면이 일부만 편집할 수 있게 해 준다. 잘못된 것은 **그 범위를 DOM 에서\n * 만든 것**이다. 목록이 늦게 오거나 잘려 있으면 화면은 그것을 모른 채 좁은 범위를 보내고,\n * 안 보낸 역할이 어떻게 되는지가 그날의 렌더링에 달린다.\n *\n * 여기서는 범위를 **상태에서** 만든다. 읽어 온 목록이 곧 범위이고, 아직 못 읽었으면 저장하지\n * 않는다.\n */\n@customElement('user-detail')\nexport class UserDetail extends localize(i18next)(LitElement) {\n /*\n * 팝업의 집 모양을 그대로 쓴다(page-standard §7·§8) — 본문만 스크롤하고 단추 줄은 `ox-popup-actions`\n * 하나다. 제목은 이 팝업이 무엇인지(사용자 상세 정보)를 말하고, 누구인지는 대상 띠(`.what`)가 말한다.\n */\n static styles: CSSResultGroup = [\n CommonPopupStyles,\n PopupBodyStyles,\n FormFieldStyles,\n css`\n /* 상태 한 줄 — 그 줄에 대한 동작은 줄 끝에 둔다(§8 「줄 하나」). */\n [state-line] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n }\n\n [state-line] [label],\n [group-label] {\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n /* 팝업 안의 컨트롤은 담는 카드를 따라 알약이다 — 공용 규칙은 단추 줄 안에만 걸린다. */\n [state-line] md-outlined-button {\n margin-left: auto;\n --md-outlined-button-container-height: var(--popup-action-height, 32px);\n --md-outlined-button-container-shape: var(--popup-control-radius);\n }\n\n [group-label] {\n margin-top: var(--spacing-medium, 8px);\n }\n\n ul[roles] {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n /*\n A role is its name with what it opens under it. Side by side, a long description squeezed the name a\n syllable a line on a phone (「직/원」, 2026-11-06).\n */\n ul[roles] li {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n column-gap: var(--spacing-medium, 8px);\n align-items: baseline;\n padding: 3px 0;\n }\n\n ul[roles] li input {\n grid-row: span 2;\n align-self: start;\n }\n\n ul[roles] li [desc] {\n grid-column: 2;\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n .note md-icon {\n --md-icon-size: 16px;\n vertical-align: -3px;\n }\n `\n ]\n\n /** 목록이 골라 준 사람. `id` 와 `username`(없으면 `email`)이 있어야 한다. */\n @property({ type: Object }) user: any\n\n @state() private availableRoles: any[] = []\n @state() private selected: Set<string> = new Set()\n @state() private loading: boolean = true\n @state() private saving: boolean = false\n\n /*\n * **내가 이 도메인에서 무엇을 할 수 있나 — 서버 선언을 그대로 되묻는다.**\n *\n * `/auth/profile` 이 `privileges`·`owner`·`super` 를 전부 담아 주고, `hasPrivilege`\n * (`auth-base/client/profiled.ts`)가 서버의 `checkPermission` 과 같은 규칙으로 그것을 판정한다\n * — 안전하지 않은 IP 에서 소유권 우회를 빼는 것까지 같다. 그래서 **서버 지시자에 적힌 인자를\n * 그대로 넘기는 것**이 화면과 문을 어긋나지 않게 하는 방법이다.\n *\n * 옛 화면은 이것을 두고 `me.email === domainOwner.email` 로 판정했다 — 목록에서 뽑은 대표\n * 소유자 한 명과 견주는 것이라 **공동 소유자가 자기 권한을 잃었고**, `user:mutation` 을 역할로\n * 받은 사람은 아예 단추를 못 봤다.\n *\n * 둘이 다른 선언을 쓴다.\n *\n * 활성/비활성·초기화·제외 user:mutation + domainOwnerGranted\n * 소유권 이전 domainOwnerGranted + superUserGranted (권한 쌍이 없다)\n *\n * ⚠ 수퍼유저가 자동으로 다 통과하지 않는다 — `checkPermission` 은 지시자가\n * `superUserGranted` 를 선언한 곳에서만 그것을 본다. 그래서 위 둘을 뭉치면 안 된다.\n */\n @state() private canManage: boolean = false\n @state() private canOwn: boolean = false\n @state() private myEmail: string = ''\n\n /**\n * 이 사람에게 이 도메인이 비밀번호 설정 링크를 발급해도 되는가 — 서버 규칙(`password-reset-rule.ts`)의 답.\n *\n * 비밀번호는 계정 하나에 하나다. 다른 도메인에서도 일하는 사람, 어느 도메인이든 오너인 사람\n * (수퍼유저 포함)의 비밀번호는 이 도메인이 거두지 않는다(ADR-0161 결정 4).\n */\n @state() private setupLinkIssuable: boolean = false\n\n /**\n * 이 도메인(과 부모)의 역할 id.\n *\n * 소유권 이전은 **받는 사람이 이 도메인에 역할을 하나라도 가질 때만** 된다\n * (`transferOwner` 가 상속 역할까지 자격으로 인정하고, 그 밖은 던진다). 파트너십으로 넘어온\n * 역할은 자격이 아니므로 `availableRoles`(합친 것)로는 판정할 수 없다.\n */\n @state() private domainRoleIds: Set<string> = new Set()\n /** The parent domain's roles this person may not change here - locked, with the reason under the list. */\n @state() private locked: Set<string> = new Set()\n private unsubscribe?: () => void\n\n /*\n * **저장할 수 있나 — 단추와 처리기가 같은 답을 본다.**\n *\n * 셋을 갈라야 한다. 화면에서 갈라 놓고 저장에서 뭉치면, 「아직 못 읽었다」와 「읽었는데\n * 역할이 없다」가 똑같이 **눌러도 아무 일이 없는 단추**가 된다. 그것이 조용히 되돌아가는\n * 자리이고, 사람은 저장이 됐는지 안 됐는지 알 수 없다.\n *\n * 못 읽었다 아직 범위를 모른다 — 눌리면 안 된다\n * 역할이 0개 보낼 것이 없다 — 눌리면 안 되고, 무엇을 해야 하는지는 목록 자리가 말한다\n * 읽었고 있다 저장한다\n */\n private get savable(): boolean {\n return !this.loading && !this.saving && this.availableRoles.length > 0\n }\n\n render() {\n const user = this.user || {}\n\n return html`\n <div class=\"body\">\n <div class=\"what\">${user.name || user.username || user.email} · ${user.email}</div>\n\n ${\n user.owner\n ? html`<div class=\"note\">\n <md-icon>supervisor_account</md-icon>\n ${i18next.t('text.owner opens everything without a role')}\n </div>`\n : nothing\n }\n ${this.renderAccountState(user)}\n\n <div group-label>${i18next.t('field.roles')}</div>\n ${this.renderRoles()}\n </div>\n\n <ox-popup-actions>\n ${this.renderIrreversible(user)}\n <md-text-button slot=\"cancel\" @click=${() => this.dispatchEvent(new CustomEvent('close'))}>\n ${i18next.t('button.cancel')}\n </md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${!this.savable} @click=${() => this.onSave()}>\n ${i18next.t('button.save')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n /**\n * 되돌릴 수 없는 조치 — 단추 줄의 왼쪽 끝, 빨간 글자(page-standard §8, `ox-popup-actions` 의 `start`).\n *\n * 옛 화면은 저장·소유권 이전·초기화·제외·비활성화를 **한 줄에 나란히** 뒀다. 매일 누르는\n * 「저장」 옆에 한 번 누르면 끝인 「제외」가 같은 크기로 있었다. 단추 줄이 그 거리를 맡는다 —\n * 저장은 오른쪽 끝, 이것들은 왼쪽 끝이고, 색은 글자에만 쓴다.\n *\n * ── 무엇을 언제 내주나 ──────────────────────────────────────────────────────\n * 조건은 전부 **서버가 실제로 던지는 것**을 그대로 옮긴 것이다. 눌리는 단추를 보여 주고\n * 서버가 거절하면 그것은 안내가 아니라 오류다.\n *\n * 비밀번호 설정 링크 서버가 된다고 하고(`mayIssuePasswordSetupLink`), 내가 아니다\n * 소유권 이전 활성 상태고, 소유자가 아니고, 이 도메인에 역할이 하나라도 있다\n * 도메인에서 제외 소유자가 아니고, 내가 아니다\n *\n * ── 이름 ────────────────────────────────────────────────────────────────────\n * `deleteDomainUser` 는 계정을 지우지 않는다 — **이 도메인에서만 뗀다**(서버 설명도 그렇게\n * 적혀 있다). 옛 화면의 「사용자 삭제」는 하지 않는 일을 말하고 있었다.\n */\n private renderIrreversible(user: any) {\n const me = this.myEmail && user.email === this.myEmail\n\n const resettable = this.canManage && this.setupLinkIssuable && !me\n const transferable =\n this.canOwn &&\n !user.owner &&\n user.status === 'activated' &&\n (user.roles || []).some((role: any) => this.domainRoleIds.has(role.id))\n const removable = this.canManage && !user.owner && !me\n\n return html`\n ${\n resettable\n ? html`<md-text-button slot=\"start\" destructive @click=${() => this.onPasswordSetup()}>\n ${user.status === 'password_reset_required'\n ? i18next.t('button.reissue setup link')\n : i18next.t('button.password reset link')}\n </md-text-button>`\n : nothing\n }\n ${\n transferable\n ? html`<md-text-button slot=\"start\" destructive @click=${() => this.onTransfer()}>\n ${i18next.t('button.transfer owner')}\n </md-text-button>`\n : nothing\n }\n ${\n removable\n ? html`<md-text-button slot=\"start\" destructive @click=${() => this.onRemoveFromDomain()}>\n ${i18next.t('button.remove from domain')}\n </md-text-button>`\n : nothing\n }\n `\n }\n\n /**\n * 계정 상태 한 줄과, 그것을 바꾸는 단추를 그 줄 끝에(page-standard §8 「줄 하나」).\n *\n * **잠금 해제와 활성화는 서버에서 같은 것이다** — `activateUser` 가 `failCount` 를 0 으로\n * 두고 `ACTIVATED` 로 되돌린다. 그래서 부르는 것은 하나이고, **글자만 상황에 맞춰 바꾼다.**\n * 잠긴 사람에게 「활성화」라고 하면 무슨 일이 일어나는지 알 수 없다.\n *\n * 비활성화는 소유자와 나 자신에게는 내주지 않는다. 서버도 같은 것을 막지만\n * (`inactivateUser` 가 소유자·admin 을 거절한다), **화면이 눌리는 단추를 보여 주고 서버가\n * 거절하면 그것은 안내가 아니라 오류다.** 되돌릴 수 있으므로 빨간 글자가 아니다.\n */\n private renderAccountState(user: any) {\n if (!this.canManage) {\n return nothing\n }\n\n return html`\n <div state-line>\n <span label>${i18next.t('field.status')}</span>\n ${statusRenderer(() => 0)(user.status, null, user)} ${this.renderStateAction(user)}\n </div>\n `\n }\n\n private renderStateAction(user: any) {\n const status = user.status\n\n if (status === 'locked') {\n return html`<md-outlined-button @click=${() => this.onActivate()}>${i18next.t('button.unlock')}</md-outlined-button>`\n }\n\n /* 설정 대기는 활성화로 열리지 않는다 — 본인이 설정 링크로 연다(단추 줄의 설정 링크). */\n if (status === 'password_reset_required') {\n return nothing\n }\n\n if (status !== 'activated') {\n return html`<md-outlined-button @click=${() => this.onActivate()}>${i18next.t('button.activate')}</md-outlined-button>`\n }\n\n /* 활성 상태 — 되돌릴 수 있는 사람에게만, 그리고 자기 자신과 소유자에게는 안 준다. */\n if (user.owner || (this.myEmail && user.email === this.myEmail)) {\n return nothing\n }\n\n return html`<md-outlined-button @click=${() => this.onInactivate()}>${i18next.t('button.deactivate')}</md-outlined-button>`\n }\n\n private renderRoles() {\n if (this.loading) {\n return html`<div class=\"note\">${i18next.t('text.loading')}</div>`\n }\n\n if (!this.availableRoles.length) {\n /*\n * 「역할이 없다」와 「못 읽었다」는 다른 말이다. 여기까지 왔다는 것은 읽었다는 뜻이므로\n * 없는 것이 맞고, 그때는 무엇을 해야 하는지를 말한다. 저장을 막고 있으므로 실패 모양이다.\n */\n return html`<div class=\"failure\">${i18next.t('text.no role defined in this domain')}</div>`\n }\n\n return html`\n <ul roles>\n ${this.availableRoles.map(\n role => html`\n <li>\n <input\n type=\"checkbox\"\n id=\"role-${role.id}\"\n .checked=${this.selected.has(role.id)}\n ?disabled=${this.locked.has(role.id)}\n @change=${(e: Event) => this.onToggle(role.id, (e.target as HTMLInputElement).checked)}\n />\n <label for=\"role-${role.id}\">${role.name}</label>\n ${role.description ? html`<span desc>${role.description}</span>` : nothing}\n </li>\n `\n )}\n </ul>\n ${this.availableRoles.some(role => this.locked.has(role.id))\n ? html`<div class=\"note\">${i18next.t('text.parent roles are changed by the parent domain')}</div>`\n : nothing}\n `\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.applyMe()\n this.unsubscribe = store.subscribe(() => this.applyMe())\n this.checkGates()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.unsubscribe?.()\n }\n\n private applyMe() {\n const me = (store.getState() as any).auth?.user || {}\n this.myEmail = me.email || ''\n }\n\n /** 서버 지시자에 적힌 인자를 그대로 되묻는다. */\n private async checkGates() {\n this.canManage = Boolean(await hasPrivilege({ category: 'user', privilege: 'mutation', domainOwnerGranted: true }))\n this.canOwn = Boolean(await hasPrivilege({ domainOwnerGranted: true, superUserGranted: true }))\n }\n\n updated(changes: Map<string, unknown>) {\n if (changes.has('user')) {\n this.fetch()\n this.fetchSetupLinkIssuable()\n }\n }\n\n /** 관리 권한이 없으면 서버가 이 질의를 거절한다 — 그때도 단추는 없다. */\n private async fetchSetupLinkIssuable() {\n const id = this.user?.id\n this.setupLinkIssuable = false\n if (!id) {\n return\n }\n\n try {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n mayIssuePasswordSetupLink(id: $id)\n }\n `,\n variables: { id },\n context: gqlContext()\n })\n\n this.setupLinkIssuable = this.user?.id === id && response.data?.mayIssuePasswordSetupLink === true\n } catch (e) {\n this.setupLinkIssuable = false\n }\n }\n\n /**\n * 부여할 수 있는 역할을 읽는다.\n *\n * **이 사람이 지금 가진 역할은 다시 묻지 않는다** — 목록이 이미 실어 왔다(`user.roles`).\n * 옛 화면은 email 로 사용자를 다시 찾았는데, 역할 이름과 달리 email 은 유일하긴 해도\n * 손에 든 `id` 를 두고 다른 열쇠로 같은 사람을 찾는 것이었다. 안 물으면 그 문제가 없다.\n *\n * **이 도메인의 역할과 파트너십으로 넘어온 역할을 한 목록으로 합친다.** 옛 화면은 그 둘을\n * 따로 그리고 저장할 때 화면에서 다시 모았는데, 합치는 자리가 화면이면 화면이 못 그린 것은\n * 저장에서도 빠진다.\n */\n private async fetch() {\n const user = this.user\n if (!user?.id) {\n return\n }\n\n this.loading = true\n\n const response = await client.query({\n query: gql`\n query ($inherited: InheritedValueType) {\n roles(inherited: $inherited) {\n items {\n id\n name\n description\n }\n }\n grantedRoles {\n role {\n id\n name\n description\n }\n }\n parentRoles: roles(inherited: Only) {\n items {\n id\n }\n }\n mayChangeParentRoles\n }\n `,\n variables: { inherited: InheritedValueType.Include },\n context: gqlContext()\n })\n\n const roles = response.data?.roles?.items || []\n const granted = (response.data?.grantedRoles || []).map((g: any) => g.role).filter(Boolean)\n\n /* 같은 역할이 두 경로로 올 수 있다 — id 로 한 번만 담는다. */\n const byId = new Map<string, any>()\n for (const role of [...roles, ...granted]) {\n byId.set(role.id, role)\n }\n\n this.availableRoles = [...byId.values()]\n /* 소유권 이전 자격은 **이 도메인(과 부모)의 역할**만 인정한다 — 합친 목록으로는 못 본다. */\n this.domainRoleIds = new Set(roles.map((role: any) => role.id))\n this.selected = new Set((user.roles || []).map((r: any) => r.id))\n this.locked = lockedParentRoles(\n (response.data?.parentRoles?.items || []).map((role: any) => role.id),\n response.data?.mayChangeParentRoles === true\n )\n this.loading = false\n }\n\n /** 잠금 해제와 활성화가 같은 부름이다 — 서버가 `failCount` 도 0 으로 되돌린다. */\n private async onActivate() {\n await this.runUserMutation(gql`\n mutation ($username: String!) {\n activateUser(username: $username)\n }\n `)\n }\n\n private async onInactivate() {\n await this.runUserMutation(gql`\n mutation ($username: String!) {\n inactivateUser(username: $username)\n }\n `)\n }\n\n /**\n * 비밀번호 설정 링크를 발급한다 — 관리자는 비밀번호를 정하지 않는다(ADR-0161).\n *\n * 무엇이 일어나는지 · 어떻게 전할지는 그 팝업이 말하고 묻는다. 발급되면 계정이 「설정 대기」가 되므로\n * 목록을 다시 읽게 알린다(`user-updated`).\n */\n private onPasswordSetup() {\n openPasswordSetupLink(this.user, {\n onIssued: () =>\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n })\n }\n\n /**\n * 이 도메인에서만 뗀다 — 계정은 남는다.\n *\n * 그래서 확인 문구도 「지웁니다」가 아니다. 이 사람은 다른 도메인에서 계속 일할 수 있고,\n * 다시 부르면 된다.\n */\n private async onRemoveFromDomain() {\n const user = this.user || {}\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.remove from domain'),\n text: i18next.t('text.they lose access to this domain but keep the account'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const done = await this.runUserMutation(gql`\n mutation ($username: String!) {\n deleteDomainUser(username: $username)\n }\n `)\n\n if (done) {\n /* 이 사람은 이제 목록에 없다 — 상세를 열어 둘 이유가 없다. */\n this.dispatchEvent(new CustomEvent('close'))\n }\n }\n\n /**\n * 소유권을 넘긴다.\n *\n * 이미 있는 `ownership-transfer-popup` 을 그대로 쓴다 — 그 안에 **이메일을 다시 적게 하는**\n * 확인이 들어 있다. 되돌릴 수 없는 것 중에서 유일하게 두 단계인 것이고, 그 판단이 맞다.\n */\n private onTransfer() {\n const popup = openPopup(\n html`<ownership-transfer-popup\n .user=${this.user}\n @ownershipTransferred=${() => {\n popup.close()\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n }}\n ></ownership-transfer-popup>`,\n {\n backdrop: true,\n size: 'large',\n title: i18next.t('title.owner_transfer')\n }\n )\n }\n\n private async runUserMutation(mutation: any, { notify = true }: { notify?: boolean } = {}) {\n const username = this.user?.username || this.user?.email\n if (!username || this.saving) {\n return false\n }\n\n this.saving = true\n\n try {\n const response = await client.mutate({ mutation, variables: { username }, context: gqlContext() })\n\n if (response.errors) {\n return false\n }\n\n /*\n * 비밀번호 초기화만 목록을 바꾸지 않는다 — 상태도 역할도 그대로다. 그때 목록을 다시\n * 읽으면 아무것도 안 달라진 화면이 깜빡이고, 사람은 뭐가 됐는지 더 모르게 된다.\n */\n if (notify) {\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n }\n\n return true\n } finally {\n this.saving = false\n }\n }\n\n private onToggle(id: string, checked: boolean) {\n const next = new Set(this.selected)\n checked ? next.add(id) : next.delete(id)\n this.selected = next\n }\n\n /**\n * **범위를 상태에서 만든다.** 읽어 온 목록이 곧 `availableRoles` 다.\n *\n * 빈 범위를 보내면 서버는 「뗄 것이 없다」로 읽어 아무것도 안 한다 — 우연히 무해한 것이지\n * 뜻한 바가 아니므로, 그 경우는 여기까지 오지 않는다(`savable`).\n */\n private async onSave() {\n /* 단추가 이미 막고 있다 — 여기 걸리면 단추와 이 자리가 어긋난 것이다. */\n if (!this.savable) {\n return\n }\n\n const user = this.user\n const username = user?.username || user?.email\n if (!username) {\n return\n }\n\n this.saving = true\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($username: String!, $availableRoles: [ObjectRef!]!, $selectedRoles: [ObjectRef!]!) {\n updateUserRoles(username: $username, availableRoles: $availableRoles, selectedRoles: $selectedRoles) {\n id\n }\n }\n `,\n variables: {\n username,\n availableRoles: this.availableRoles.map(role => ({ id: role.id })),\n selectedRoles: this.availableRoles.filter(role => this.selected.has(role.id)).map(role => ({ id: role.id }))\n },\n context: gqlContext()\n })\n\n if (!response.errors) {\n this.dispatchEvent(new CustomEvent('user-updated', { detail: user, bubbles: true, composed: true }))\n }\n } finally {\n this.saving = false\n }\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * User management on a phone (2026-11-06). A role in the user popup is its name with what it opens under it - side by
5
+ * side, a long description squeezed the name a syllable a line. The owner's mark and name are one piece in a field,
6
+ * which is a flex box: as two pieces the icon stood in the middle of a card's title line and the name at its end.
7
+ */
8
+ const read = (file) => readFileSync(path.join(__dirname, file), 'utf-8');
9
+ describe('user management on a phone', () => {
10
+ it('a role stands as its name with its description under it', () => {
11
+ const detail = read('user-detail.ts');
12
+ expect(/ul\[roles\] li \{\s*display: grid;\s*grid-template-columns: auto minmax\(0, 1fr\);/.test(detail)).toBe(true);
13
+ expect(/ul\[roles\] li \[desc\] \{\s*grid-column: 2;/.test(detail)).toBe(true);
14
+ });
15
+ it("the owner's mark and name are one piece", () => {
16
+ const list = read('../pages/user/user-list-page.ts');
17
+ expect(list.includes('html`<span><md-icon style="--md-icon-size: 16px; vertical-align: -3px;">supervisor_account</md-icon> ${value}</span>`')).toBe(true);
18
+ });
19
+ });
20
+ //# sourceMappingURL=user-list-phone.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-list-phone.test.js","sourceRoot":"","sources":["../../client/components/user-list-phone.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB;;;;GAIG;AAEH,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;AAEhF,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAA;QACrC,MAAM,CAAC,oFAAoF,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACpH,MAAM,CAAC,8CAA8C,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAChF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,IAAI,GAAG,IAAI,CAAC,iCAAiC,CAAC,CAAA;QACpD,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,uHAAuH,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC3J,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readFileSync } from 'fs'\nimport path from 'path'\n\n/**\n * User management on a phone (2026-11-06). A role in the user popup is its name with what it opens under it - side by\n * side, a long description squeezed the name a syllable a line. The owner's mark and name are one piece in a field,\n * which is a flex box: as two pieces the icon stood in the middle of a card's title line and the name at its end.\n */\n\nconst read = (file: string) => readFileSync(path.join(__dirname, file), 'utf-8')\n\ndescribe('user management on a phone', () => {\n it('a role stands as its name with its description under it', () => {\n const detail = read('user-detail.ts')\n expect(/ul\\[roles\\] li \\{\\s*display: grid;\\s*grid-template-columns: auto minmax\\(0, 1fr\\);/.test(detail)).toBe(true)\n expect(/ul\\[roles\\] li \\[desc\\] \\{\\s*grid-column: 2;/.test(detail)).toBe(true)\n })\n\n it(\"the owner's mark and name are one piece\", () => {\n const list = read('../pages/user/user-list-page.ts')\n expect(list.includes('html`<span><md-icon style=\"--md-icon-size: 16px; vertical-align: -3px;\">supervisor_account</md-icon> ${value}</span>`')).toBe(true)\n })\n})\n"]}
@@ -441,10 +441,14 @@ UserListPage = __decorate([
441
441
  customElement('user-list-page')
442
442
  ], UserListPage);
443
443
  export { UserListPage };
444
- /** 소유자는 이름 옆에 표시한다. 서버가 사람마다 `owner` 를 채워 준다(공동 소유자도 함께). */
444
+ /**
445
+ * 소유자는 이름 옆에 표시한다. 서버가 사람마다 `owner` 를 채워 준다(공동 소유자도 함께).
446
+ * One span: a field is a flex box, and an icon and a name as two pieces stood apart - the icon in the middle of a
447
+ * card's title line, the name at its right end (2026-11-06).
448
+ */
445
449
  function nameWithOwnerMark(value, _column, record) {
446
450
  return record?.owner
447
- ? html `<md-icon style="--md-icon-size: 16px; vertical-align: -3px;">supervisor_account</md-icon> ${value}`
451
+ ? html `<span><md-icon style="--md-icon-size: 16px; vertical-align: -3px;">supervisor_account</md-icon> ${value}</span>`
448
452
  : html `${value}`;
449
453
  }
450
454
  /** 역할이 없는 것은 빈 칸이 아니라 **문장**으로 말한다. */
@@ -1 +1 @@
1
- {"version":3,"file":"user-list-page.js","sourceRoot":"","sources":["../../../client/pages/user/user-list-page.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,gCAAgC,CAAA;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAA;AACzE,OAAO,qBAAqB,CAAA;AAC5B,OAAO,sCAAsC,CAAA;AAC7C,OAAO,sCAAsC,CAAA;AAC7C,OAAO,iCAAiC,CAAA;AACxC,OAAO,iCAAiC,CAAA;AACxC,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAA;AAErF,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AAExC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAEzE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACpC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACtB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;AAGnD,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,gBAAgB;IAA3C;;QAkCL;;;WAGG;QACyB,SAAI,GAA6B,cAAc,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;QAE9E,wBAAmB,GAAW,EAAE,CAAA;QAEjD;;;WAGG;QACc,eAAU,GAAoC,SAAS,CAAA;QACxE,uEAAuE;QACtD,cAAS,GAAY,IAAI,CAAA;QACzB,UAAK,GAAW,CAAC,CAAA;QACjB,aAAQ,GAAY,KAAK,CAAA;IA0U5C,CAAC;aA3XQ,WAAM,GAAmB;QAC9B,qEAAqE;QACrE,qBAAqB;QACrB,eAAe;QACf,iBAAiB;QACjB,kBAAkB;QAClB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;KAuBF;KACF,AA9BY,CA8BZ;IAuBD,IAAI,OAAO;QACT;;;;;;WAMG;QACH,OAAO;YACL,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;YACxC,IAAI,EAAE,YAAY;YAClB;;;;eAIG;YACH,MAAM,EAAE;gBACN,OAAO,EAAE,CAAC,MAAc,EAAE,EAAE;oBAC1B,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,MAAM,CAAA;gBAChC,CAAC;gBACD,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE;aACpC;YACD,kEAAkE;YAClE,MAAM,EAAE;gBACN,OAAO,EAAE,GAAG,EAAE;oBACZ,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAA;gBAC7B,CAAC;aACF;YACD,OAAO,EAAE;gBACP;oBACE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,sCAAsC,CAAC;oBACxD,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,iBAAiB,CAAC;oBACzC,IAAI,EAAE,OAAO;iBACd;gBACD;oBACE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC;oBAClC,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE;oBAC7B,GAAG,kBAAkB,CAAC,MAAM;oBAC5B,8EAA8E;oBAC9E,IAAI,EAAE,YAAY;iBACnB;aACF;SACF,CAAA;IACH,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;;;gBAGC,IAAI,CAAC,IAAI;kBACP,IAAI,CAAC,MAAM;wBACL,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;kCAClB,IAAI,CAAC,yBAAyB,CAAC,UAAU,CAAE;;;;;;;;;;;QAWrE,IAAI,CAAC,UAAU,EAAE;KACpB,CAAA;IACH,CAAC;IAED;;;;;OAKG;IACK,UAAU;QAChB,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YAClC,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,IAAI,IAAI,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YACjC;;;cAGE;YACF,OAAO,IAAI,CAAA;gCACe,IAAI,CAAC,SAAS,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;aACpE,CAAA;QACT,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACvC,kDAAkD;YAClD,OAAO,IAAI,CAAA;;;kBAGC,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC;wBAC7B,OAAO,CAAC,CAAC,CAAC,4CAA4C,CAAC;;aAElE,CAAA;QACT,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,IAAI,CAAC,MAAM,GAAG;YACZ;;eAEG;YACH,IAAI,EAAE;gBACJ,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;gBACzB,OAAO,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC;aAC7B;YACD,OAAO,EAAE;gBACP,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE;gBACvD;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,MAAM;oBACZ,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;oBAC/B,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAAE;oBACtD,MAAM,EAAE,QAAQ;oBAChB,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,OAAO;oBACb,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;oBAChC,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;oBACzB,MAAM,EAAE,QAAQ;oBAChB,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE;;;;uBAIG;oBACH,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,QAAQ;oBACd,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;oBACjC,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,EAAE;oBACrF;;;;uBAIG;oBACH,MAAM,EAAE;wBACN,IAAI,EAAE,QAAQ;wBACd,OAAO,EAAE;4BACP,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;4BAC1B,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE;4BAClE,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE;4BAC5D,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE;yBACjE;wBACD,QAAQ,EAAE,IAAI;qBACf;oBACD,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE;;;uBAGG;oBACH,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,OAAO;oBACb,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;oBAChC,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE;oBAClD,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,UAAU;oBAChB,IAAI,EAAE,WAAW;oBACjB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC;oBACrC,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE;oBAC3B,QAAQ,EAAE,IAAI;oBACd,KAAK,EAAE,GAAG;iBACX;aACF;YACD,IAAI,EAAE;gBACJ,+FAA+F;gBAE/F,UAAU,EAAE,KAAK;gBACjB,UAAU,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;gBAC/B,QAAQ,EAAE;oBACR,KAAK,EAAE,CAAC,QAAa,EAAE,KAAU,EAAE,OAAY,EAAE,MAAW,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;iBAC3F;aACF;YACD,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;SAC5B,CAAA;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAY,EAAE,SAAc;QAC5C,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,CAAC,cAAc,CAAA;YACzB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QACpB,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,GAAG,EAAE,EAAe;QAC1E,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;QACrB;sDAC8C;QAC9C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAA;QAElC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,CACf,MAAM,MAAM,CAAC,KAAK,CAAC;gBACjB,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;WAqBT;gBACD,SAAS,EAAE;oBACT,OAAO,EAAE;wBACP,GAAG,OAAO;wBACV;4BACE,IAAI,EAAE,UAAU;4BAChB,QAAQ,EAAE,OAAO;4BACjB,KAAK,EAAE,CAAC,aAAa,EAAE,WAAW,CAAC;yBACpC;qBACF;oBACD,UAAU,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;oBAC3B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;iBAC1D;aACF,CAAC,CACH,CAAC,IAAI,CAAC,SAAS,CAAA;YAEhB,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAA;YAC3B,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAA;YAE1B,OAAO;gBACL,OAAO,EAAE,QAAQ,CAAC,KAAK;gBACvB,KAAK,EAAE,QAAQ,CAAC,KAAK;aACtB,CAAA;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,4BAA4B;YAC5B,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAA;YAC1B,IAAI,CAAC,SAAS,GAAG,CAAC,CAAA;YAClB,MAAM,CAAC,CAAA;QACT,CAAC;IACH,CAAC;IAED;;;;;;;;OAQG;IACK,YAAY,CAAC,MAAW;QAC9B,MAAM,KAAK,GAAG,SAAS,CACrB,IAAI,CAAA;;kBAEQ,MAAM;mBACL,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE;0BACZ,GAAG,EAAE;YACnB,KAAK,CAAC,KAAK,EAAE,CAAA;YACb,kDAAkD;YAClD,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QACpB,CAAC;;OAEJ,EACD;YACE,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC;SACvC,CACF,CAAA;IACH,CAAC;IAED;;;;OAIG;IACK,QAAQ;QACd,MAAM,KAAK,GAAG,SAAS,CACrB,IAAI,CAAA;;mBAES,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE;0BACZ,CAAC,CAAc,EAAE,EAAE;YACjC,KAAK,CAAC,KAAK,EAAE,CAAA;YACb,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;YAElB,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,CAAA;YACjC,IAAI,OAAO,EAAE,CAAC;gBACZ,qBAAqB,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACxE,CAAC;QACH,CAAC;;OAEJ,EACD;YACE,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC;SACnC,CACF,CAAA;IACH,CAAC;;AA1V2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;4CAAY;AAKX;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;0CAAoE;AAE9E;IAAhB,KAAK,EAAE;;yDAAyC;AAMhC;IAAhB,KAAK,EAAE;;gDAAgE;AAEvD;IAAhB,KAAK,EAAE;;+CAAkC;AACzB;IAAhB,KAAK,EAAE;;2CAA0B;AACjB;IAAhB,KAAK,EAAE;;8CAAkC;AAEf;IAA1B,KAAK,CAAC,UAAU,CAAC;;2CAA0B;AApDjC,YAAY;IADxB,aAAa,CAAC,gBAAgB,CAAC;GACnB,YAAY,CA4XxB;;AAED,6DAA6D;AAC7D,SAAS,iBAAiB,CAAC,KAAa,EAAE,OAAY,EAAE,MAAW;IACjE,OAAO,MAAM,EAAE,KAAK;QAClB,CAAC,CAAC,IAAI,CAAA,6FAA6F,KAAK,EAAE;QAC1G,CAAC,CAAC,IAAI,CAAA,GAAG,KAAK,EAAE,CAAA;AACpB,CAAC;AAED,uCAAuC;AACvC,SAAS,aAAa,CAAC,MAAW,EAAE,OAAY,EAAE,MAAW;IAC3D,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,EAAE,CAAA;IAEjC,OAAO,KAAK,CAAC,MAAM;QACjB,CAAC,CAAC,IAAI,CAAA,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;QAC1D,CAAC,CAAC,IAAI,CAAA,gCAAgC,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC,eAAe,CAAA;AAC3F,CAAC","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../../components/auth-badge.js'\nimport { statusRenderer } from '../../components/user-status-renderer.js'\nimport '@operato/data-grist'\nimport '@operato/list-state/ox-empty-note.js'\nimport '@operato/list-state/ox-page-state.js'\nimport '../../components/user-detail.js'\nimport '../../components/user-invite.js'\nimport { openPasswordSetupLink } from '../../components/password-setup-link-popup.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, nothing } from 'lit'\nimport type { CSSResultGroup } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { openPopup } from '@operato/layout'\nimport { i18next, localize } from '@operato/i18n'\nimport { p13n } from '@operato/p13n'\nimport { navigate, PageView } from '@operato/shell'\nimport {\n CommonButtonStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n WorkSurfacePageStyles\n} from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\n\n/**\n * 사용자 목록 — **서버가 거르고, 서버가 쪽을 나눈다.**\n *\n * ── 옛 화면이 하던 것 (`pages/user/user-management.ts`) ──────────────────────\n * `query { users { items { … } } }` 로 **도메인의 모든 사용자**를 한 번에 가져왔다. 한도도\n * 서버 검색도 없었고, 프레임워크가 이미 그것을 경고하고 있었다\n * (`unbounded list query on \"User\"`). 사용자는 권한(84행)과 달리 실제로 늘어난다.\n *\n * 그런데 **서버는 처음부터 받을 준비가 되어 있었다** — `users(filters:, pagination:, sortings:)`\n * 가 `ListParam` 을 받고 `name`·`email`·`description` 을 검색어로 본다\n * (`user-query.ts`). 화면이 안 쓰고 있었을 뿐이다.\n *\n * ── 왜 `ox-grist` 인가 ──────────────────────────────────────────────────────\n * 손으로 만든 목록이 잃는 것을 그것이 이미 갖고 있다 — 쪽 나누기, 정렬, 컬럼 걸러 보기,\n * 사람마다 기억하는 개인 설정, 카드↔표 모드, 내보내기. `figure-list-page` 가 같은 이유로\n * 옮겼고 그 판단을 그대로 따른다.\n *\n * ── 옛 화면의 탭을 없앤 이유 ────────────────────────────────────────────────\n * 탭이 `userType` 값으로 갈렸다. 그런데 그 칸은 `varchar` 이고 코드가 예상하는 넷\n * (`admin`·`user`·`application`·`appliance`) 밖의 값이거나 비어 있으면 **그 사람이 어느 탭에도\n * 안 나온다 — 화면에서 조용히 사라진다.**\n *\n * 여기서는 사람만 싣는다. 애플리케이션과 어플라이언스는 자기 화면(`applications`·`appliance`)에\n * 있고, 거기서 자기 자격증명까지 함께 다루는 것이 맞다. 다만 **거르는 것은 서버가 해야 한다** —\n * 지금은 `users` 가 종류를 안 가리므로 이 화면이 filter 로 요구한다.\n */\n/*\n * `as typeof PageView` 로 조이지 않는다 — 그렇게 하면 p13n 이 얹어 주는\n * `getPagePreferenceProvider` 가 타입에서 사라져 개인 설정(모드·정렬·쪽 크기를 사람마다\n * 기억하는 것)을 붙일 수 없다. `figure-list-page` 가 같은 주석을 달고 있다.\n */\nconst UserListPageBase = p13n(localize(i18next)(PageView))\n\n@customElement('user-list-page')\nexport class UserListPage extends UserListPageBase {\n static styles: CSSResultGroup = [\n /* The work surface frame for a list screen (ADR-0060 decision 8). */\n WorkSurfacePageStyles,\n ScrollbarStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n css`\n /* The note below is laid out against the page, over the grid. */\n :host {\n position: relative;\n }\n\n /*\n ── 상태 배지 ──────────────────────────────────────────────────────────\n 색은 **짝으로** 쓴다(container / on-container). 한쪽만 쓰면 야간에 대비가 깨진다.\n 그리고 색은 「무엇이 나를 기다리나」에만 쓴다 — 활성은 기본값이라 색이 없다.\n 담는 면이 행이므로 모서리는 작게 잡는다.\n */\n\n /* 격자를 떼지 않고 위에 겹친다 — 격자가 살아 있어야 다시 읽을 수 있다. */\n div[note] {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: var(--ui-bg, var(--md-sys-color-surface));\n }\n `\n ]\n\n @property({ type: Object }) config: any\n /*\n * 카드가 아니라 **행**이다. 카드는 그림으로 알아보는 물건(형상 카탈로그)의 모양이고,\n * 사람은 이름으로 알아본다. 좁은 화면에서는 `LIST` 가 같은 행을 접어 그린다.\n */\n @property({ type: String }) mode: 'CARD' | 'GRID' | 'LIST' = isMobileDevice() ? 'LIST' : 'GRID'\n\n @state() private lockDurationMinutes: number = 10\n\n /*\n * **읽는 중 · 못 읽었다 · 읽었는데 없다 를 가른다.** 셋을 한 문장으로 그리면 못 읽은 것을\n * 「없다」로 읽고, 사용자가 이미 있는 것을 만들러 간다.\n */\n @state() private fetchState: 'loading' | 'failed' | 'loaded' = 'loading'\n /** Why the read failed. The screen shows the server's own sentence. */\n @state() private loadError: unknown = null\n @state() private total: number = 0\n @state() private filtered: boolean = false\n\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n /*\n * Calling someone in is what this screen is for, and it belongs in the page action bar\n * (ADR-0060 decision 9). It used to float over the list. A button that does the same thing in a\n * different place on every screen makes what you learned on one screen useless on the next.\n * Which of the two acts it is - adding an account that already exists, or making one that waits\n * for its password - is said by the window it opens.\n */\n return {\n title: i18next.t('text.user management'),\n help: 'auth/users',\n /*\n * Full text search sits in the title bar, the way every other list screen in these\n * applications carries it (user instruction, 2026-09-18). It searches the columns that\n * declare a search filter - here the name and the address.\n */\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n /* Narrowing by state stays in the headroom, and this folds it. */\n filter: {\n handler: () => {\n this.grist.toggleHeadroom()\n }\n },\n actions: [\n {\n title: i18next.t('access-request.title.access requests'),\n action: () => navigate('access-requests'),\n icon: 'gavel'\n },\n {\n title: i18next.t('label.add user'),\n action: () => this.onInvite(),\n ...CommonButtonStyles.submit,\n /* The mark says what the button does: call someone in, rather than submit. */\n icon: 'person_add'\n }\n ]\n }\n }\n\n render() {\n return html`\n <ox-grist\n class=\"page-fill\"\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <div class=\"filters\">\n <ox-filters-form without-search autofocus></ox-filters-form>\n </div>\n </div>\n\n <ox-grist-personalizer slot=\"setting\"></ox-grist-personalizer>\n </ox-grist>\n\n ${this.renderNote()}\n `\n }\n\n /**\n * 목록 위에 겹쳐 그린다 — 격자를 떼면 다시 읽을 길이 없어진다.\n *\n * 거른 결과가 비어 있는 것은 **가르칠 자리가 아니다.** 그때는 격자가 스스로 「없음」을\n * 말하게 두고, 여기서는 아무것도 그리지 않는다.\n */\n private renderNote() {\n if (this.fetchState === 'loading') {\n return nothing\n }\n\n if (this.fetchState === 'failed') {\n /*\n Tells a refusal from a breakage, carries the server's own sentence, and offers to read it\n again (decision 8). It used to be one note for everything: \"failed to fetch, try again\".\n */\n return html`<div note>\n <ox-page-state .error=${this.loadError} @retry=${() => this.grist.fetch()}></ox-page-state>\n </div>`\n }\n\n if (this.total === 0 && !this.filtered) {\n /* 새 도메인의 첫 화면. 소유자는 역할 없이도 모두 열리므로 그 사실부터 말한다. */\n return html`<div note>\n <ox-empty-note\n icon=\"group_add\"\n title=${i18next.t('text.no other user yet')}\n description=${i18next.t('text.owner opens everything without a role')}\n ></ox-empty-note>\n </div>`\n }\n\n return nothing\n }\n\n async pageInitialized() {\n this.config = {\n /*\n * 카드에 실을 것. 표 모드도 같은 컬럼 설정을 쓰므로 **두 모드가 다른 말을 하지 않는다.**\n */\n list: {\n fields: ['name', 'email'],\n details: ['status', 'roles']\n },\n columns: [\n { type: 'gutter', gutterName: 'sequence', fixed: true },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('field.name'),\n record: { align: 'left', renderer: nameWithOwnerMark },\n filter: 'search',\n sortable: true,\n width: 160\n },\n {\n type: 'string',\n name: 'email',\n header: i18next.t('field.email'),\n record: { align: 'left' },\n filter: 'search',\n sortable: true,\n width: 220\n },\n {\n /*\n * 상태 하나로 두 가지를 말한다 — 지금 상태와, 잠긴 경우 **언제 풀리는지**.\n * `lockedAt` 과 설정된 잠금 시간으로 계산한다. 옛 화면은 「잠김」까지만 말했고,\n * 그래서 관리자가 기다리면 되는지 풀어 줘야 하는지 알 수 없었다.\n */\n type: 'string',\n name: 'status',\n header: i18next.t('field.status'),\n record: { align: 'center', renderer: statusRenderer(() => this.lockDurationMinutes) },\n /*\n * The editor a filter gets comes from the filter's own `type`, and without one the form\n * falls back to a text box - so this list of states was declared and never drawn. The\n * states are shown by the same words the cell uses, not by their stored spelling.\n */\n filter: {\n type: 'select',\n options: [\n { display: '', value: '' },\n { display: i18next.t('label.activated-user'), value: 'activated' },\n { display: i18next.t('label.locked-user'), value: 'locked' },\n { display: i18next.t('label.inactive-user'), value: 'inactive' }\n ],\n operator: 'eq'\n },\n sortable: true,\n width: 140\n },\n {\n /*\n * 역할이 비어 있는 것을 **눈에 띄게** 그린다. 역할 없는 활성 계정은 로그인은 되는데\n * 화면이 하나도 없다 — 옛 화면에서는 그것이 빈 칸이라 아무도 알아채지 못했다.\n */\n type: 'string',\n name: 'roles',\n header: i18next.t('field.roles'),\n record: { align: 'left', renderer: rolesRenderer },\n width: 260\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated_at'),\n record: { align: 'center' },\n sortable: true,\n width: 160\n }\n ],\n rows: {\n /* Read only here - a new one is made elsewhere, so the blank row at the end offers nothing. */\n\n appendable: false,\n selectable: { multiple: false },\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: any) => this.onSelectUser(record)\n }\n },\n sorters: [{ name: 'name' }]\n }\n }\n\n async pageUpdated(changes: any, lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n }\n }\n\n /**\n * 사람만 가져온다. **거르는 것은 서버가 한다** — 옛 화면은 전부 받아 클라이언트에서 나눴다.\n *\n * `userType` 은 `varchar` 라 값이 넷 밖으로 나갈 수 있다. 그래서 「기계가 아닌 것」을\n * `not-in` 으로 요구한다 — 「사람인 것」을 `in` 으로 요구하면 값이 비어 있는 사람이 사라진다.\n * (`userType` 을 enum 으로 못 박는 것은 §3 의 일이다.)\n */\n async fetchHandler({ filters = [], page, limit, sortings = [] }: FetchOption) {\n this.fetchState = 'loading'\n this.loadError = null\n /* 사람만 보려고 이 화면이 얹는 조건은 「거른 것」으로 세지 않는다 — 그것까지 세면\n 아무도 없는 도메인이 「거른 결과가 없다」로 보여 첫 안내가 안 나온다. */\n this.filtered = filters.length > 0\n\n try {\n const response = (\n await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n responses: users(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n username\n name\n email\n userType\n status\n lockedAt\n owner\n roles {\n id\n name\n }\n updatedAt\n }\n total\n }\n }\n `,\n variables: {\n filters: [\n ...filters,\n {\n name: 'userType',\n operator: 'notin',\n value: ['application', 'appliance']\n }\n ],\n pagination: { page, limit },\n sortings: sortings.length ? sortings : [{ name: 'name' }]\n }\n })\n ).data.responses\n\n this.total = response.total\n this.fetchState = 'loaded'\n\n return {\n records: response.items,\n total: response.total\n }\n } catch (e) {\n /* 못 읽은 것을 「없다」로 그리지 않는다. */\n this.fetchState = 'failed'\n this.loadError = e\n throw e\n }\n }\n\n /**\n * 고른 사람의 역할을 고치는 자리를 연다.\n *\n * `domain-management` 가 격자 행에서 `ox-attribute-view` 를 여는 것과 같은 모양이다 —\n * `handlers.click` 에서 `openPopup` 에 template 을 넘긴다.\n *\n * `escapable` 은 넘기지 않는다. 이 앱의 기본값이 `false` 이고, 그것이 곧 **ESC 에 먼저\n * 묻는다**는 뜻이다 — 편집면에서 원하는 거동이 기본값이다.\n */\n private onSelectUser(record: any) {\n const popup = openPopup(\n html`\n <user-detail\n .user=${record}\n @close=${() => popup.close()}\n @user-updated=${() => {\n popup.close()\n /* 고친 사람만 다시 읽을 길이 없다 — 격자가 가진 것은 쪽 단위 새로고침뿐이다. */\n this.grist.fetch()\n }}\n ></user-detail>\n `,\n {\n backdrop: true,\n size: 'large',\n title: i18next.t('title.user details')\n }\n )\n }\n\n /**\n * 목록의 역할 편집과 같은 모양으로 연다 — `openPopup` 에 template 을 넘긴다.\n *\n * 새로 만든 계정은 「비밀번호 설정 대기」다. 그 자리에서 설정 링크를 건네도록 이어서 연다(ADR-0161).\n */\n private onInvite() {\n const popup = openPopup(\n html`\n <user-invite\n @close=${() => popup.close()}\n @user-invited=${(e: CustomEvent) => {\n popup.close()\n this.grist.fetch()\n\n const created = e.detail?.created\n if (created) {\n openPasswordSetupLink(created, { onIssued: () => this.grist.fetch() })\n }\n }}\n ></user-invite>\n `,\n {\n backdrop: true,\n size: 'large',\n title: i18next.t('label.add user')\n }\n )\n }\n}\n\n/** 소유자는 이름 옆에 표시한다. 서버가 사람마다 `owner` 를 채워 준다(공동 소유자도 함께). */\nfunction nameWithOwnerMark(value: string, _column: any, record: any) {\n return record?.owner\n ? html`<md-icon style=\"--md-icon-size: 16px; vertical-align: -3px;\">supervisor_account</md-icon> ${value}`\n : html`${value}`\n}\n\n/** 역할이 없는 것은 빈 칸이 아니라 **문장**으로 말한다. */\nfunction rolesRenderer(_value: any, _column: any, record: any) {\n const roles = record?.roles || []\n\n return roles.length\n ? html`${roles.map((role: any) => role.name).join(' · ')}`\n : html`<auth-badge tone=\"attention\">${i18next.t('text.no role assigned')}</auth-badge>`\n}\n"]}
1
+ {"version":3,"file":"user-list-page.js","sourceRoot":"","sources":["../../../client/pages/user/user-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../../components/auth-badge.js'\nimport { statusRenderer } from '../../components/user-status-renderer.js'\nimport '@operato/data-grist'\nimport '@operato/list-state/ox-empty-note.js'\nimport '@operato/list-state/ox-page-state.js'\nimport '../../components/user-detail.js'\nimport '../../components/user-invite.js'\nimport { openPasswordSetupLink } from '../../components/password-setup-link-popup.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, nothing } from 'lit'\nimport type { CSSResultGroup } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { openPopup } from '@operato/layout'\nimport { i18next, localize } from '@operato/i18n'\nimport { p13n } from '@operato/p13n'\nimport { navigate, PageView } from '@operato/shell'\nimport {\n CommonButtonStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n WorkSurfacePageStyles\n} from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\n\n/**\n * 사용자 목록 — **서버가 거르고, 서버가 쪽을 나눈다.**\n *\n * ── 옛 화면이 하던 것 (`pages/user/user-management.ts`) ──────────────────────\n * `query { users { items { … } } }` 로 **도메인의 모든 사용자**를 한 번에 가져왔다. 한도도\n * 서버 검색도 없었고, 프레임워크가 이미 그것을 경고하고 있었다\n * (`unbounded list query on \"User\"`). 사용자는 권한(84행)과 달리 실제로 늘어난다.\n *\n * 그런데 **서버는 처음부터 받을 준비가 되어 있었다** — `users(filters:, pagination:, sortings:)`\n * 가 `ListParam` 을 받고 `name`·`email`·`description` 을 검색어로 본다\n * (`user-query.ts`). 화면이 안 쓰고 있었을 뿐이다.\n *\n * ── 왜 `ox-grist` 인가 ──────────────────────────────────────────────────────\n * 손으로 만든 목록이 잃는 것을 그것이 이미 갖고 있다 — 쪽 나누기, 정렬, 컬럼 걸러 보기,\n * 사람마다 기억하는 개인 설정, 카드↔표 모드, 내보내기. `figure-list-page` 가 같은 이유로\n * 옮겼고 그 판단을 그대로 따른다.\n *\n * ── 옛 화면의 탭을 없앤 이유 ────────────────────────────────────────────────\n * 탭이 `userType` 값으로 갈렸다. 그런데 그 칸은 `varchar` 이고 코드가 예상하는 넷\n * (`admin`·`user`·`application`·`appliance`) 밖의 값이거나 비어 있으면 **그 사람이 어느 탭에도\n * 안 나온다 — 화면에서 조용히 사라진다.**\n *\n * 여기서는 사람만 싣는다. 애플리케이션과 어플라이언스는 자기 화면(`applications`·`appliance`)에\n * 있고, 거기서 자기 자격증명까지 함께 다루는 것이 맞다. 다만 **거르는 것은 서버가 해야 한다** —\n * 지금은 `users` 가 종류를 안 가리므로 이 화면이 filter 로 요구한다.\n */\n/*\n * `as typeof PageView` 로 조이지 않는다 — 그렇게 하면 p13n 이 얹어 주는\n * `getPagePreferenceProvider` 가 타입에서 사라져 개인 설정(모드·정렬·쪽 크기를 사람마다\n * 기억하는 것)을 붙일 수 없다. `figure-list-page` 가 같은 주석을 달고 있다.\n */\nconst UserListPageBase = p13n(localize(i18next)(PageView))\n\n@customElement('user-list-page')\nexport class UserListPage extends UserListPageBase {\n static styles: CSSResultGroup = [\n /* The work surface frame for a list screen (ADR-0060 decision 8). */\n WorkSurfacePageStyles,\n ScrollbarStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n css`\n /* The note below is laid out against the page, over the grid. */\n :host {\n position: relative;\n }\n\n /*\n ── 상태 배지 ──────────────────────────────────────────────────────────\n 색은 **짝으로** 쓴다(container / on-container). 한쪽만 쓰면 야간에 대비가 깨진다.\n 그리고 색은 「무엇이 나를 기다리나」에만 쓴다 — 활성은 기본값이라 색이 없다.\n 담는 면이 행이므로 모서리는 작게 잡는다.\n */\n\n /* 격자를 떼지 않고 위에 겹친다 — 격자가 살아 있어야 다시 읽을 수 있다. */\n div[note] {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: var(--ui-bg, var(--md-sys-color-surface));\n }\n `\n ]\n\n @property({ type: Object }) config: any\n /*\n * 카드가 아니라 **행**이다. 카드는 그림으로 알아보는 물건(형상 카탈로그)의 모양이고,\n * 사람은 이름으로 알아본다. 좁은 화면에서는 `LIST` 가 같은 행을 접어 그린다.\n */\n @property({ type: String }) mode: 'CARD' | 'GRID' | 'LIST' = isMobileDevice() ? 'LIST' : 'GRID'\n\n @state() private lockDurationMinutes: number = 10\n\n /*\n * **읽는 중 · 못 읽었다 · 읽었는데 없다 를 가른다.** 셋을 한 문장으로 그리면 못 읽은 것을\n * 「없다」로 읽고, 사용자가 이미 있는 것을 만들러 간다.\n */\n @state() private fetchState: 'loading' | 'failed' | 'loaded' = 'loading'\n /** Why the read failed. The screen shows the server's own sentence. */\n @state() private loadError: unknown = null\n @state() private total: number = 0\n @state() private filtered: boolean = false\n\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n /*\n * Calling someone in is what this screen is for, and it belongs in the page action bar\n * (ADR-0060 decision 9). It used to float over the list. A button that does the same thing in a\n * different place on every screen makes what you learned on one screen useless on the next.\n * Which of the two acts it is - adding an account that already exists, or making one that waits\n * for its password - is said by the window it opens.\n */\n return {\n title: i18next.t('text.user management'),\n help: 'auth/users',\n /*\n * Full text search sits in the title bar, the way every other list screen in these\n * applications carries it (user instruction, 2026-09-18). It searches the columns that\n * declare a search filter - here the name and the address.\n */\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n /* Narrowing by state stays in the headroom, and this folds it. */\n filter: {\n handler: () => {\n this.grist.toggleHeadroom()\n }\n },\n actions: [\n {\n title: i18next.t('access-request.title.access requests'),\n action: () => navigate('access-requests'),\n icon: 'gavel'\n },\n {\n title: i18next.t('label.add user'),\n action: () => this.onInvite(),\n ...CommonButtonStyles.submit,\n /* The mark says what the button does: call someone in, rather than submit. */\n icon: 'person_add'\n }\n ]\n }\n }\n\n render() {\n return html`\n <ox-grist\n class=\"page-fill\"\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <div class=\"filters\">\n <ox-filters-form without-search autofocus></ox-filters-form>\n </div>\n </div>\n\n <ox-grist-personalizer slot=\"setting\"></ox-grist-personalizer>\n </ox-grist>\n\n ${this.renderNote()}\n `\n }\n\n /**\n * 목록 위에 겹쳐 그린다 — 격자를 떼면 다시 읽을 길이 없어진다.\n *\n * 거른 결과가 비어 있는 것은 **가르칠 자리가 아니다.** 그때는 격자가 스스로 「없음」을\n * 말하게 두고, 여기서는 아무것도 그리지 않는다.\n */\n private renderNote() {\n if (this.fetchState === 'loading') {\n return nothing\n }\n\n if (this.fetchState === 'failed') {\n /*\n Tells a refusal from a breakage, carries the server's own sentence, and offers to read it\n again (decision 8). It used to be one note for everything: \"failed to fetch, try again\".\n */\n return html`<div note>\n <ox-page-state .error=${this.loadError} @retry=${() => this.grist.fetch()}></ox-page-state>\n </div>`\n }\n\n if (this.total === 0 && !this.filtered) {\n /* 새 도메인의 첫 화면. 소유자는 역할 없이도 모두 열리므로 그 사실부터 말한다. */\n return html`<div note>\n <ox-empty-note\n icon=\"group_add\"\n title=${i18next.t('text.no other user yet')}\n description=${i18next.t('text.owner opens everything without a role')}\n ></ox-empty-note>\n </div>`\n }\n\n return nothing\n }\n\n async pageInitialized() {\n this.config = {\n /*\n * 카드에 실을 것. 표 모드도 같은 컬럼 설정을 쓰므로 **두 모드가 다른 말을 하지 않는다.**\n */\n list: {\n fields: ['name', 'email'],\n details: ['status', 'roles']\n },\n columns: [\n { type: 'gutter', gutterName: 'sequence', fixed: true },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('field.name'),\n record: { align: 'left', renderer: nameWithOwnerMark },\n filter: 'search',\n sortable: true,\n width: 160\n },\n {\n type: 'string',\n name: 'email',\n header: i18next.t('field.email'),\n record: { align: 'left' },\n filter: 'search',\n sortable: true,\n width: 220\n },\n {\n /*\n * 상태 하나로 두 가지를 말한다 — 지금 상태와, 잠긴 경우 **언제 풀리는지**.\n * `lockedAt` 과 설정된 잠금 시간으로 계산한다. 옛 화면은 「잠김」까지만 말했고,\n * 그래서 관리자가 기다리면 되는지 풀어 줘야 하는지 알 수 없었다.\n */\n type: 'string',\n name: 'status',\n header: i18next.t('field.status'),\n record: { align: 'center', renderer: statusRenderer(() => this.lockDurationMinutes) },\n /*\n * The editor a filter gets comes from the filter's own `type`, and without one the form\n * falls back to a text box - so this list of states was declared and never drawn. The\n * states are shown by the same words the cell uses, not by their stored spelling.\n */\n filter: {\n type: 'select',\n options: [\n { display: '', value: '' },\n { display: i18next.t('label.activated-user'), value: 'activated' },\n { display: i18next.t('label.locked-user'), value: 'locked' },\n { display: i18next.t('label.inactive-user'), value: 'inactive' }\n ],\n operator: 'eq'\n },\n sortable: true,\n width: 140\n },\n {\n /*\n * 역할이 비어 있는 것을 **눈에 띄게** 그린다. 역할 없는 활성 계정은 로그인은 되는데\n * 화면이 하나도 없다 — 옛 화면에서는 그것이 빈 칸이라 아무도 알아채지 못했다.\n */\n type: 'string',\n name: 'roles',\n header: i18next.t('field.roles'),\n record: { align: 'left', renderer: rolesRenderer },\n width: 260\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated_at'),\n record: { align: 'center' },\n sortable: true,\n width: 160\n }\n ],\n rows: {\n /* Read only here - a new one is made elsewhere, so the blank row at the end offers nothing. */\n\n appendable: false,\n selectable: { multiple: false },\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: any) => this.onSelectUser(record)\n }\n },\n sorters: [{ name: 'name' }]\n }\n }\n\n async pageUpdated(changes: any, lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n }\n }\n\n /**\n * 사람만 가져온다. **거르는 것은 서버가 한다** — 옛 화면은 전부 받아 클라이언트에서 나눴다.\n *\n * `userType` 은 `varchar` 라 값이 넷 밖으로 나갈 수 있다. 그래서 「기계가 아닌 것」을\n * `not-in` 으로 요구한다 — 「사람인 것」을 `in` 으로 요구하면 값이 비어 있는 사람이 사라진다.\n * (`userType` 을 enum 으로 못 박는 것은 §3 의 일이다.)\n */\n async fetchHandler({ filters = [], page, limit, sortings = [] }: FetchOption) {\n this.fetchState = 'loading'\n this.loadError = null\n /* 사람만 보려고 이 화면이 얹는 조건은 「거른 것」으로 세지 않는다 — 그것까지 세면\n 아무도 없는 도메인이 「거른 결과가 없다」로 보여 첫 안내가 안 나온다. */\n this.filtered = filters.length > 0\n\n try {\n const response = (\n await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n responses: users(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n username\n name\n email\n userType\n status\n lockedAt\n owner\n roles {\n id\n name\n }\n updatedAt\n }\n total\n }\n }\n `,\n variables: {\n filters: [\n ...filters,\n {\n name: 'userType',\n operator: 'notin',\n value: ['application', 'appliance']\n }\n ],\n pagination: { page, limit },\n sortings: sortings.length ? sortings : [{ name: 'name' }]\n }\n })\n ).data.responses\n\n this.total = response.total\n this.fetchState = 'loaded'\n\n return {\n records: response.items,\n total: response.total\n }\n } catch (e) {\n /* 못 읽은 것을 「없다」로 그리지 않는다. */\n this.fetchState = 'failed'\n this.loadError = e\n throw e\n }\n }\n\n /**\n * 고른 사람의 역할을 고치는 자리를 연다.\n *\n * `domain-management` 가 격자 행에서 `ox-attribute-view` 를 여는 것과 같은 모양이다 —\n * `handlers.click` 에서 `openPopup` 에 template 을 넘긴다.\n *\n * `escapable` 은 넘기지 않는다. 이 앱의 기본값이 `false` 이고, 그것이 곧 **ESC 에 먼저\n * 묻는다**는 뜻이다 — 편집면에서 원하는 거동이 기본값이다.\n */\n private onSelectUser(record: any) {\n const popup = openPopup(\n html`\n <user-detail\n .user=${record}\n @close=${() => popup.close()}\n @user-updated=${() => {\n popup.close()\n /* 고친 사람만 다시 읽을 길이 없다 — 격자가 가진 것은 쪽 단위 새로고침뿐이다. */\n this.grist.fetch()\n }}\n ></user-detail>\n `,\n {\n backdrop: true,\n size: 'large',\n title: i18next.t('title.user details')\n }\n )\n }\n\n /**\n * 목록의 역할 편집과 같은 모양으로 연다 — `openPopup` 에 template 을 넘긴다.\n *\n * 새로 만든 계정은 「비밀번호 설정 대기」다. 그 자리에서 설정 링크를 건네도록 이어서 연다(ADR-0161).\n */\n private onInvite() {\n const popup = openPopup(\n html`\n <user-invite\n @close=${() => popup.close()}\n @user-invited=${(e: CustomEvent) => {\n popup.close()\n this.grist.fetch()\n\n const created = e.detail?.created\n if (created) {\n openPasswordSetupLink(created, { onIssued: () => this.grist.fetch() })\n }\n }}\n ></user-invite>\n `,\n {\n backdrop: true,\n size: 'large',\n title: i18next.t('label.add user')\n }\n )\n }\n}\n\n/**\n * 소유자는 이름 옆에 표시한다. 서버가 사람마다 `owner` 를 채워 준다(공동 소유자도 함께).\n * One span: a field is a flex box, and an icon and a name as two pieces stood apart - the icon in the middle of a\n * card's title line, the name at its right end (2026-11-06).\n */\nfunction nameWithOwnerMark(value: string, _column: any, record: any) {\n return record?.owner\n ? html`<span><md-icon style=\"--md-icon-size: 16px; vertical-align: -3px;\">supervisor_account</md-icon> ${value}</span>`\n : html`${value}`\n}\n\n/** 역할이 없는 것은 빈 칸이 아니라 **문장**으로 말한다. */\nfunction rolesRenderer(_value: any, _column: any, record: any) {\n const roles = record?.roles || []\n\n return roles.length\n ? html`${roles.map((role: any) => role.name).join(' · ')}`\n : html`<auth-badge tone=\"attention\">${i18next.t('text.no role assigned')}</auth-badge>`\n}\n"]}