@things-factory/auth-ui 10.1.226 → 10.1.228

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 (37) hide show
  1. package/dist-client/components/role-template-picker.d.ts +5 -0
  2. package/dist-client/components/role-template-picker.js +151 -42
  3. package/dist-client/components/role-template-picker.js.map +1 -1
  4. package/dist-client/components/role-template-picker.test.d.ts +1 -0
  5. package/dist-client/components/role-template-picker.test.js +41 -0
  6. package/dist-client/components/role-template-picker.test.js.map +1 -0
  7. package/dist-client/pages/role/role-add-delete.test.d.ts +1 -0
  8. package/dist-client/pages/role/role-add-delete.test.js +61 -0
  9. package/dist-client/pages/role/role-add-delete.test.js.map +1 -0
  10. package/dist-client/pages/role/role-inheritance-shown.test.d.ts +1 -0
  11. package/dist-client/pages/role/role-inheritance-shown.test.js +31 -0
  12. package/dist-client/pages/role/role-inheritance-shown.test.js.map +1 -0
  13. package/dist-client/pages/role/role-list-page.d.ts +15 -11
  14. package/dist-client/pages/role/role-list-page.js +112 -53
  15. package/dist-client/pages/role/role-list-page.js.map +1 -1
  16. package/dist-client/pages/role/role-privilege-inherited.test.d.ts +1 -0
  17. package/dist-client/pages/role/role-privilege-inherited.test.js +23 -0
  18. package/dist-client/pages/role/role-privilege-inherited.test.js.map +1 -0
  19. package/dist-client/pages/role/role-privilege-page.d.ts +8 -5
  20. package/dist-client/pages/role/role-privilege-page.js +23 -13
  21. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  22. package/dist-client/pages/role/role-privilege-views.test.d.ts +1 -0
  23. package/dist-client/pages/role/role-privilege-views.test.js +19 -0
  24. package/dist-client/pages/role/role-privilege-views.test.js.map +1 -0
  25. package/dist-client/tsconfig.tsbuildinfo +1 -1
  26. package/helps/auth/application.ko.md +1 -1
  27. package/helps/auth/auth-provider.ko.md +1 -1
  28. package/helps/auth/domain-owners.ko.md +1 -1
  29. package/helps/operato-hub/partners.ko.md +1 -1
  30. package/helps/system/attribute.ko.md +1 -1
  31. package/package.json +6 -6
  32. package/translations/en.json +17 -10
  33. package/translations/es.json +17 -10
  34. package/translations/ja.json +17 -10
  35. package/translations/ko.json +18 -11
  36. package/translations/ms.json +17 -10
  37. package/translations/zh.json +17 -10
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-add-delete.test.js","sourceRoot":"","sources":["../../../client/pages/role/role-add-delete.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from 'fs'\nimport path from 'path'\n\n/**\n * How a role is added and deleted on the role list (the user, 2026-10-28: 「도대체 어떻게 추가하고, 삭제한다는\n * 말인가」). One way to add - the 「역할 추가」 window, no blank row in the grid - and deleting acts on picked rows,\n * says first which picked roles people still hold, and deletes none of them. The page needs a DOM, so the test\n * reads its source with comments stripped.\n */\n\nconst read = (file: string) => readFileSync(path.join(__dirname, file), 'utf-8')\nconst strip = (source: string) => source.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/^\\s*\\/\\/.*$/gm, '').replace(/<!--[\\s\\S]*?-->/g, '')\n\nconst code = strip(read('role-list-page.ts'))\nconst deleting = code.slice(code.indexOf('private async onDeleteGrid('), code.indexOf('private async openCreate('))\nconst words = JSON.parse(readFileSync(path.join(__dirname, '../../../translations/ko.json'), 'utf-8'))\n\ndescribe('adding a role', () => {\n it('★ the button says what it adds', () => {\n expect(code).toMatch(/<button add @click=\\$\\{\\(\\) => this\\.openCreate\\(\\)\\}>\\s*<md-icon>add<\\/md-icon>\\$\\{i18next\\.t\\('button\\.add role'\\)\\}/)\n expect(words['button.add role']).toBe('역할 추가')\n })\n\n it('★ the grid draws no blank row to type a role into', () => {\n expect(code).toMatch(/rows: \\{\\s*appendable: false,/)\n })\n\n it('the window is titled the same', () => {\n expect(code).toMatch(/title: i18next\\.t\\('button\\.add role'\\)/)\n })\n})\n\ndescribe('deleting a role', () => {\n it('★ rows can be picked', () => {\n expect(code).toMatch(/\\{ type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true \\}/)\n })\n\n it('★ the list asks how many people hold each role and shows it', () => {\n expect(code).toMatch(/privilegeCount\\s+userCount\\s+templateGap/)\n expect(code).toMatch(/name: 'userCount',[\\s\\S]{0,120}renderer: userCountCell/)\n })\n\n it('★ names the picked roles people hold and stops before asking the server', () => {\n const held = deleting.indexOf(\"record.userCount > 0\")\n expect(held).toBeGreaterThan(0)\n expect(deleting.indexOf('deleteRoles(ids')).toBeGreaterThan(held)\n expect(deleting.slice(held, deleting.indexOf('deleteRoles(ids'))).toMatch(/text\\.roles held by users are not deleted[\\s\\S]*?return\\s*\\}/)\n })\n\n it('★ names picked roles inherited from the parent and stops - even mixed with this domain\\'s own', () => {\n const inherited = deleting.indexOf('this.isInherited(record)')\n expect(inherited).toBeGreaterThan(0)\n expect(deleting.slice(inherited, deleting.indexOf('deleteRoles(ids'))).toMatch(/if \\(inherited\\.length\\) \\{[\\s\\S]*?text\\.inherited roles are not deleted here[\\s\\S]*?return\\s*\\}/)\n expect(deleting).toMatch(/const mine = selected\\n/)\n })\n\n it('says to pick rows when none are picked, instead of doing nothing', () => {\n expect(deleting).toMatch(/if \\(!selected\\?\\.length\\) \\{\\s*await OxPrompt\\.open\\([\\s\\S]*?text\\.pick the roles to delete/)\n })\n\n it('names what it is about to delete before deleting it', () => {\n expect(deleting).toMatch(/text\\.delete these roles', \\{ names: mine\\.map/)\n })\n\n it('every sentence has its words in every language', () => {\n const keys = ['button.add role', 'field.role users', 'text.n people', 'text.pick the roles to delete', 'text.roles held by users are not deleted', 'text.role and its users', 'text.delete these roles', 'text.inherited roles are not deleted here']\n for (const language of ['ko', 'en', 'ja', 'zh', 'es', 'ms']) {\n const all = JSON.parse(readFileSync(path.join(__dirname, `../../../translations/${language}.json`), 'utf-8'))\n for (const key of keys) expect([language, key, typeof all[key]]).toEqual([language, key, 'string'])\n }\n })\n})\n"]}
@@ -0,0 +1,31 @@
1
+ import { readFileSync } from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * A domain that inherits from nowhere shows nothing about inheritance - no 「우리 역할 · 상속 · 전체」
5
+ * buttons, no line about where roles come from, no source column (the user, 2026-10-28). The page
6
+ * needs a DOM, so the test reads its source with comments stripped.
7
+ */
8
+ const source = readFileSync(path.join(__dirname, 'role-list-page.ts'), 'utf-8');
9
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
10
+ const scopes = code.slice(code.indexOf('private renderScopes('), code.indexOf('private renderLineage('));
11
+ const initialized = code.slice(code.indexOf('async pageInitialized('), code.indexOf('async pageUpdated('));
12
+ describe('the role list in a domain with no parent', () => {
13
+ it('★ inherits only when the profile names a parent - not knowing is not having none', () => {
14
+ expect(code).toMatch(/private hasParent\(\): boolean \{\s*return this\.parentDomain !== null\s*\}/);
15
+ });
16
+ it('★ draws no scope buttons, and so no lineage line', () => {
17
+ expect(scopes).toMatch(/^private renderScopes\(\) \{\s*if \(!this\.hasParent\(\)\) \{\s*return nothing\s*\}/);
18
+ expect(code.match(/this\.renderLineage\(\)/g)).toHaveLength(1);
19
+ expect(scopes).toMatch(/this\.renderLineage\(\)/);
20
+ });
21
+ it('★ leaves the source column and its list detail out', () => {
22
+ expect(initialized).toMatch(/const inheriting = this\.hasParent\(\)/);
23
+ expect(initialized).toMatch(/details: inheriting \? \['privilegeCount', 'userCount', 'source'\] : \['privilegeCount', 'userCount'\]/);
24
+ expect(initialized).toMatch(/\.\.\.\(inheriting\s*\?\s*\[\s*\{\s*type: 'string',\s*name: 'source',/);
25
+ expect(initialized.match(/name: 'source'/g)).toHaveLength(1);
26
+ });
27
+ it('counts the scopes only where there are scopes to count', () => {
28
+ expect(initialized).toMatch(/if \(this\.hasParent\(\)\) \{\s*this\.countScopes\(\)\s*\}/);
29
+ });
30
+ });
31
+ //# sourceMappingURL=role-inheritance-shown.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-inheritance-shown.test.js","sourceRoot":"","sources":["../../../client/pages/role/role-inheritance-shown.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB;;;;GAIG;AAEH,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,EAAE,OAAO,CAAC,CAAA;AAC/E,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;AAEjF,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,uBAAuB,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,CAAA;AACxG,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,wBAAwB,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAA;AAE1G,QAAQ,CAAC,0CAA0C,EAAE,GAAG,EAAE;IACxD,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,6EAA6E,CAAC,CAAA;IACrG,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,qFAAqF,CAAC,CAAA;QAC7G,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAC9D,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,wCAAwC,CAAC,CAAA;QACrE,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,wGAAwG,CAAC,CAAA;QACrI,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,uEAAuE,CAAC,CAAA;QACpG,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,4DAA4D,CAAC,CAAA;IAC3F,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readFileSync } from 'fs'\nimport path from 'path'\n\n/**\n * A domain that inherits from nowhere shows nothing about inheritance - no 「우리 역할 · 상속 · 전체」\n * buttons, no line about where roles come from, no source column (the user, 2026-10-28). The page\n * needs a DOM, so the test reads its source with comments stripped.\n */\n\nconst source = readFileSync(path.join(__dirname, 'role-list-page.ts'), 'utf-8')\nconst code = source.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/^\\s*\\/\\/.*$/gm, '')\n\nconst scopes = code.slice(code.indexOf('private renderScopes('), code.indexOf('private renderLineage('))\nconst initialized = code.slice(code.indexOf('async pageInitialized('), code.indexOf('async pageUpdated('))\n\ndescribe('the role list in a domain with no parent', () => {\n it('★ inherits only when the profile names a parent - not knowing is not having none', () => {\n expect(code).toMatch(/private hasParent\\(\\): boolean \\{\\s*return this\\.parentDomain !== null\\s*\\}/)\n })\n\n it('★ draws no scope buttons, and so no lineage line', () => {\n expect(scopes).toMatch(/^private renderScopes\\(\\) \\{\\s*if \\(!this\\.hasParent\\(\\)\\) \\{\\s*return nothing\\s*\\}/)\n expect(code.match(/this\\.renderLineage\\(\\)/g)).toHaveLength(1)\n expect(scopes).toMatch(/this\\.renderLineage\\(\\)/)\n })\n\n it('★ leaves the source column and its list detail out', () => {\n expect(initialized).toMatch(/const inheriting = this\\.hasParent\\(\\)/)\n expect(initialized).toMatch(/details: inheriting \\? \\['privilegeCount', 'userCount', 'source'\\] : \\['privilegeCount', 'userCount'\\]/)\n expect(initialized).toMatch(/\\.\\.\\.\\(inheriting\\s*\\?\\s*\\[\\s*\\{\\s*type: 'string',\\s*name: 'source',/)\n expect(initialized.match(/name: 'source'/g)).toHaveLength(1)\n })\n\n it('counts the scopes only where there are scopes to count', () => {\n expect(initialized).toMatch(/if \\(this\\.hasParent\\(\\)\\) \\{\\s*this\\.countScopes\\(\\)\\s*\\}/)\n })\n})\n"]}
@@ -105,19 +105,22 @@ export declare class RoleListPage extends RoleListPageBase {
105
105
  */
106
106
  private renderScopes;
107
107
  /**
108
- * 어디에서 물려받나 — **있을 때나 없을 때나 같은 자리에서 말한다.**
108
+ * 어디에서 물려받나 — **어느 도메인인지**를 적는다. 그 역할을 고치러 가야 할 곳이다.
109
109
  *
110
- * 「상속」 단추만 두면 눌러 보기 전에는 그것이 무엇을 뜻하는지 알 수 없다. 부모가 있는
111
- * 도메인에서는 **어느 도메인인지**가 필요하고(그 역할을 고치러 가야 할 곳이다), 최상위
112
- * 도메인에서는 **물려받는 곳이 없다는 사실**이 필요하다. 후자를 안 적으면 「상속 0」이
113
- * 「아직 안 만들었나」로 읽힌다.
110
+ * 묶음 단추와 함께 그려지므로 부모가 있을 때만 온다(`hasParent`).
111
+ */
112
+ private renderLineage;
113
+ /**
114
+ * 물려받는 도메인인가 — 상속을 말하는 것(묶음 단추 · 물려받는 곳 · 출처 칸)이 모두 이것을 본다.
114
115
  *
115
- * 단추를 숨기지 않는다. 숨기면 최상위 도메인의 관리자는 상속이라는 것이 있다는 사실
116
- * 자체를 모르고, 자식 도메인을 만든 뒤에 갑자기 나타나는 단추를 만난다.
116
+ * 최상위 도메인에는 상속이 없다. 「우리 역할 9 · 상속 0 · 전체 9」와 줄마다 「우리」는 같은 말을
117
+ * 되풀이할 뿐이라 그리지 않는다(사용자 2026-10-28: 「이 도메인이 상속을 받는 도메인이 아니라면,
118
+ * 굳이 상속과 관련된 정보를 보일 필요가 없을 것이다」).
117
119
  *
118
- * 아직 프로필을 못 읽었으면 **아무것도 안 적는다** — 모르는 것을 「없다」로 적지 않는다.
120
+ * 아직 모르면(`undefined`, 옛 서버가 `parent` 칸을 안 보낼 때) 그대로 그린다 — 모르는 것을
121
+ * 「없다」로 접지 않는다.
119
122
  */
120
- private renderLineage;
123
+ private hasParent;
121
124
  private onScope;
122
125
  /**
123
126
  * 세 묶음의 건수.
@@ -188,8 +191,9 @@ export declare class RoleListPage extends RoleListPageBase {
188
191
  /**
189
192
  * 고른 역할을 지운다.
190
193
  *
191
- * 서버는 **가진 사람이 있는 역할을 남긴다**(`deleteRoles`). 그래서 여기서 「지웠다」고 말하지
192
- * 않고 목록을 다시 읽는다 — 무엇이 남았는지가 화면에서 보이는 것이 맞다.
194
+ * 가진 사람이 있는 역할은 서버가 거절한다(`role-in-use`) — 하나라도 있으면 아무것도 안 지운다. 화면은
195
+ * 보내기 전에 같은 것을 사람 수 칸으로 알고 있으므로 먼저 그 이름을 말하고 멈춘다. 서버의 거절은 그 사이
196
+ * 누군가 부여했을 때를 위한 것이다.
193
197
  */
194
198
  private onDeleteGrid;
195
199
  /**
@@ -167,11 +167,15 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
167
167
  * 같은 줄의 문법이 깨진다. 범위 단추와 같은 모서리·같은 테두리를 쓰되, **여는 일**이므로
168
168
  * 채워서 한 단 앞으로 낸다 — 나머지는 거르는 일이고 이것만 만드는 일이다.
169
169
  */
170
+ /* 「+」 하나로는 무엇을 더하는지 몰랐다(2026-10-28 사용자: 「'+' 라고 하지 말고 '역할 추가' 라고 하자」). */
170
171
  button[add] {
171
172
  display: inline-flex;
172
173
  align-items: center;
173
174
  justify-content: center;
174
- padding: 5px 10px;
175
+ gap: 4px;
176
+ padding: 5px 12px 5px 8px;
177
+ font: inherit;
178
+ font-size: var(--fontsize-small, 13px);
175
179
  color: var(--md-sys-color-on-secondary-container);
176
180
  background-color: var(--md-sys-color-secondary-container);
177
181
  border: 1px solid transparent;
@@ -254,8 +258,8 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
254
258
  여기 ox-record-creator 가 있었다 — 목록에 빈 줄을 만들고 이름을 치게 하는
255
259
  관례(24 파일)다. 걷어낸 이유는 openCreate 의 머리말에 있다.
256
260
  -->
257
- <button add @click=${() => this.openCreate()} title=${i18next.t('button.create')}>
258
- <md-icon>add</md-icon>
261
+ <button add @click=${() => this.openCreate()}>
262
+ <md-icon>add</md-icon>${i18next.t('button.add role')}
259
263
  </button>
260
264
  </div>
261
265
  </div>
@@ -275,6 +279,9 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
275
279
  * 아직 못 센 묶음은 수를 **안 그린다** — 0 으로 메우면 「없다」와 같아 보인다.
276
280
  */
277
281
  renderScopes() {
282
+ if (!this.hasParent()) {
283
+ return nothing;
284
+ }
278
285
  const scopes = [
279
286
  [InheritedValueType.None, 'label.ours'],
280
287
  [InheritedValueType.Only, 'label.inherited'],
@@ -293,30 +300,26 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
293
300
  `;
294
301
  }
295
302
  /**
296
- * 어디에서 물려받나 — **있을 때나 없을 때나 같은 자리에서 말한다.**
303
+ * 어디에서 물려받나 — **어느 도메인인지**를 적는다. 그 역할을 고치러 가야 할 곳이다.
297
304
  *
298
- * 「상속」 단추만 두면 눌러 보기 전에는 그것이 무엇을 뜻하는지 알 수 없다. 부모가 있는
299
- * 도메인에서는 **어느 도메인인지**가 필요하고(그 역할을 고치러 가야 할 곳이다), 최상위
300
- * 도메인에서는 **물려받는 곳이 없다는 사실**이 필요하다. 후자를 안 적으면 「상속 0」이
301
- * 「아직 안 만들었나」로 읽힌다.
302
- *
303
- * 단추를 숨기지 않는다. 숨기면 최상위 도메인의 관리자는 상속이라는 것이 있다는 사실
304
- * 자체를 모르고, 자식 도메인을 만든 뒤에 갑자기 나타나는 단추를 만난다.
305
- *
306
- * 아직 프로필을 못 읽었으면 **아무것도 안 적는다** — 모르는 것을 「없다」로 적지 않는다.
305
+ * 묶음 단추와 함께 그려지므로 부모가 있을 때만 온다(`hasParent`).
307
306
  */
308
307
  renderLineage() {
309
- if (this.parentDomain === undefined) {
310
- return nothing;
311
- }
312
308
  const from = this.parentDomain?.name || this.parentDomain?.subdomain;
313
- return html `
314
- <span lineage>
315
- ${from
316
- ? i18next.t('text.inherited from x', { x: from })
317
- : i18next.t('text.this domain is top level and inherits from nowhere')}
318
- </span>
319
- `;
309
+ return html `<span lineage>${i18next.t('text.inherited from x', { x: from })}</span>`;
310
+ }
311
+ /**
312
+ * 물려받는 도메인인가 — 상속을 말하는 것(묶음 단추 · 물려받는 곳 · 출처 칸)이 모두 이것을 본다.
313
+ *
314
+ * 최상위 도메인에는 상속이 없다. 「우리 역할 9 · 상속 0 · 전체 9」와 줄마다 「우리」는 같은 말을
315
+ * 되풀이할 뿐이라 그리지 않는다(사용자 2026-10-28: 「이 도메인이 상속을 받는 도메인이 아니라면,
316
+ * 굳이 상속과 관련된 정보를 보일 필요가 없을 것이다」).
317
+ *
318
+ * 아직 모르면(`undefined`, 옛 서버가 `parent` 칸을 안 보낼 때) 그대로 그린다 — 모르는 것을
319
+ * 「없다」로 접지 않는다.
320
+ */
321
+ hasParent() {
322
+ return this.parentDomain !== null;
320
323
  }
321
324
  onScope(scope) {
322
325
  if (this.scope === scope) {
@@ -421,7 +424,10 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
421
424
  * 안 적는다. 「모른다」를 「없다」로 접지 않는다.
422
425
  */
423
426
  this.parentDomain = mine && 'parent' in mine ? mine.parent : undefined;
424
- this.countScopes();
427
+ if (this.hasParent()) {
428
+ this.countScopes();
429
+ }
430
+ const inheriting = this.hasParent();
425
431
  this.config = {
426
432
  /*
427
433
  * **두 모드가 다른 말을 하지 않게 한다.**
@@ -435,10 +441,12 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
435
441
  */
436
442
  list: {
437
443
  fields: ['name', 'description'],
438
- details: ['privilegeCount', 'source']
444
+ details: inheriting ? ['privilegeCount', 'userCount', 'source'] : ['privilegeCount', 'userCount']
439
445
  },
440
446
  columns: [
441
447
  { type: 'gutter', gutterName: 'sequence', fixed: true },
448
+ /* 지우기가 고른 행에 닿는다 — 고르는 칸이 없으면 「삭제」는 무엇을 지우는지 말할 수 없다. */
449
+ { type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true },
442
450
  {
443
451
  /*
444
452
  * **권한 편집으로 가는 문.**
@@ -533,6 +541,18 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
533
541
  record: { align: 'center', renderer: privilegeCountCell },
534
542
  width: 110
535
543
  },
544
+ {
545
+ /*
546
+ * **이 역할을 가진 사람이 몇 명인가.** 가진 사람이 있는 역할은 지우지 않으므로(`deleteRoles`),
547
+ * 지우기 전에 이 수가 무엇이 지워지지 않을지 말한다(2026-10-28 사용자: 「각 역할이 부여된 숫자를
548
+ * 보여주는 것이 좋겠다」).
549
+ */
550
+ type: 'string',
551
+ name: 'userCount',
552
+ header: i18next.t('field.role users'),
553
+ record: { align: 'center', renderer: userCountCell },
554
+ width: 100
555
+ },
536
556
  {
537
557
  /*
538
558
  * **템플릿이 그 뒤로 더 얻은 것** (ADR-0075 결정 1).
@@ -551,25 +571,29 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
551
571
  record: { align: 'center', renderer: (_v, _c, record) => this.templateGapCell(record) },
552
572
  width: 150
553
573
  },
554
- {
555
- /*
556
- * **출처 — 여기가 「보이는 것과 고칠 수 있는 것」을 가른다.**
557
- *
558
- * 전에는 도메인 이름을 날값으로 냈다(`SYSTEM` · `MIRATEK`). 그건 구분이 아니다 —
559
- * 읽는 사람이 자기 도메인 이름을 외우고 있어야 한다. 지금은 「우리」와
560
- * 「상속 · SYSTEM」 으로 말한다.
561
- *
562
- * ⚠ `classifier` 로 행 전체를 물들이지 않는다. 선례들이 색을 `'#8B0000'` 처럼 박아
563
- * 두었고(dataset 5곳), 그 규약(`emphasized: [배경, 글자]`)이 `var(...)` 를 받는지도
564
- * 확인되지 않았다. 그리고 `emphasized` 는 **강조**하는 장치인데 여기서 필요한 것은
565
- * 그 반대다 — 11행 중 9행을 강조하면 강조가 아니다. 그래서 이 칸이 진다.
566
- */
567
- type: 'string',
568
- name: 'source',
569
- header: i18next.t('field.source'),
570
- record: { align: 'center', renderer: (_v, _c, record) => this.sourceCell(record) },
571
- width: 150
572
- },
574
+ ...(inheriting
575
+ ? [
576
+ {
577
+ /*
578
+ * **출처 — 여기가 「보이는 것과 고칠 수 있는 것」을 가른다.**
579
+ *
580
+ * 전에는 도메인 이름을 날값으로 냈다(`SYSTEM` · `MIRATEK`). 그건 구분이 아니다 —
581
+ * 읽는 사람이 자기 도메인 이름을 외우고 있어야 한다. 지금은 「우리」와
582
+ * 「상속 · SYSTEM」 으로 말한다.
583
+ *
584
+ * ⚠ `classifier` 로 행 전체를 물들이지 않는다. 선례들이 색을 `'#8B0000'` 처럼 박아
585
+ * 두었고(dataset 5곳), 그 규약(`emphasized: [배경, 글자]`)이 `var(...)` 를 받는지도
586
+ * 확인되지 않았다. 그리고 `emphasized` 는 **강조**하는 장치인데 여기서 필요한 것은
587
+ * 그 반대다 — 11행 중 9행을 강조하면 강조가 아니다. 그래서 이 칸이 진다.
588
+ */
589
+ type: 'string',
590
+ name: 'source',
591
+ header: i18next.t('field.source'),
592
+ record: { align: 'center', renderer: (_v, _c, record) => this.sourceCell(record) },
593
+ width: 150
594
+ }
595
+ ]
596
+ : []),
573
597
  {
574
598
  type: 'datetime',
575
599
  name: 'updatedAt',
@@ -580,6 +604,8 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
580
604
  }
581
605
  ],
582
606
  rows: {
607
+ /* 역할은 「역할 추가」 창으로만 선다 — 격자 맨 아래 빈 줄은 이름만 받는 두 번째 문이었다. */
608
+ appendable: false,
583
609
  selectable: { multiple: true }
584
610
  },
585
611
  sorters: [{ name: 'name' }]
@@ -636,6 +662,7 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
636
662
  subdomain
637
663
  }
638
664
  privilegeCount
665
+ userCount
639
666
  templateGap {
640
667
  templateId
641
668
  templateName
@@ -716,24 +743,47 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
716
743
  /**
717
744
  * 고른 역할을 지운다.
718
745
  *
719
- * 서버는 **가진 사람이 있는 역할을 남긴다**(`deleteRoles`). 그래서 여기서 「지웠다」고 말하지
720
- * 않고 목록을 다시 읽는다 — 무엇이 남았는지가 화면에서 보이는 것이 맞다.
746
+ * 가진 사람이 있는 역할은 서버가 거절한다(`role-in-use`) — 하나라도 있으면 아무것도 안 지운다. 화면은
747
+ * 보내기 전에 같은 것을 사람 수 칸으로 알고 있으므로 먼저 그 이름을 말하고 멈춘다. 서버의 거절은 그 사이
748
+ * 누군가 부여했을 때를 위한 것이다.
721
749
  */
722
750
  async onDeleteGrid() {
723
751
  const selected = this.grist.selected;
724
752
  if (!selected?.length) {
753
+ await OxPrompt.open({
754
+ type: 'info',
755
+ title: i18next.t('button.delete'),
756
+ text: i18next.t('text.pick the roles to delete'),
757
+ confirmButton: { text: i18next.t('button.confirm') }
758
+ });
725
759
  return;
726
760
  }
727
761
  /*
728
762
  * **상속된 행은 여기서 못 지운다** — `deleteRoles` 가 `domain.id` 로 좁혀 찾으므로 그 행은
729
- * 조용히 남는다. 「지웠다」로 보이고 목록에 그대로 있으면 사람이 두 번 세 번 누른다.
763
+ * 조용히 남는다. 우리 역할과 섞여 골랐을 때도 빼고 지우지 않는다 — 그러면 확인 창이 고른 것과
764
+ * 다른 것을 지우겠다고 말한다. 이름을 대고 멈춘다(가진 사람이 있는 역할과 같은 규칙).
730
765
  */
731
- const mine = selected.filter((record) => !this.isInherited(record));
732
- if (!mine.length) {
766
+ const inherited = selected.filter((record) => this.isInherited(record));
767
+ if (inherited.length) {
733
768
  await OxPrompt.open({
734
769
  type: 'warning',
735
770
  title: i18next.t('button.delete'),
736
- text: i18next.t('text.inherited roles are edited in their own domain'),
771
+ text: i18next.t('text.inherited roles are not deleted here', {
772
+ names: inherited.map((record) => record.name).join(', ')
773
+ }),
774
+ confirmButton: { text: i18next.t('button.confirm') }
775
+ });
776
+ return;
777
+ }
778
+ const mine = selected;
779
+ const held = mine.filter((record) => record.userCount > 0);
780
+ if (held.length) {
781
+ await OxPrompt.open({
782
+ type: 'warning',
783
+ title: i18next.t('button.delete'),
784
+ text: i18next.t('text.roles held by users are not deleted', {
785
+ names: held.map((record) => i18next.t('text.role and its users', { name: record.name, n: record.userCount })).join(', ')
786
+ }),
737
787
  confirmButton: { text: i18next.t('button.confirm') }
738
788
  });
739
789
  return;
@@ -741,8 +791,8 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
741
791
  if (!(await OxPrompt.open({
742
792
  type: 'warning',
743
793
  title: i18next.t('button.delete'),
744
- text: i18next.t('text.roles in use are kept'),
745
- confirmButton: { text: i18next.t('button.confirm') },
794
+ text: i18next.t('text.delete these roles', { names: mine.map((record) => record.name).join(', ') }),
795
+ confirmButton: { text: i18next.t('button.delete') },
746
796
  cancelButton: { text: i18next.t('button.cancel') }
747
797
  }))) {
748
798
  return;
@@ -784,7 +834,7 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
784
834
  const popup = openPopup(html `<role-template-picker .page=${this} @close=${() => popup.close()}></role-template-picker>`, {
785
835
  backdrop: true,
786
836
  size: 'large',
787
- title: i18next.t('button.create role')
837
+ title: i18next.t('button.add role')
788
838
  });
789
839
  }
790
840
  /** 템플릿에서 — 이름은 이 공장이 정한 것을 그대로 보낸다. */
@@ -968,4 +1018,13 @@ function privilegeCountCell(_value, _column, record) {
968
1018
  ? html `<auth-badge tone="attention">${i18next.t('text.no privilege granted')}</auth-badge>`
969
1019
  : html `<auth-badge>${count}</auth-badge>`;
970
1020
  }
1021
+ /** 가진 사람 수 한 칸 — 0 은 조용히, 가진 사람이 있으면(지울 수 없으면) 보통 무게로. */
1022
+ function userCountCell(_value, _column, record) {
1023
+ const count = record?.userCount;
1024
+ /* 「모른다」와 「0」은 다르다 — 아직 안 온 값은 그리지 않는다. */
1025
+ if (count === undefined || count === null) {
1026
+ return html ``;
1027
+ }
1028
+ return html `<auth-badge tone=${count ? nothing : 'quiet'}>${i18next.t('text.n people', { n: count })}</auth-badge>`;
1029
+ }
971
1030
  //# sourceMappingURL=role-list-page.js.map