@things-factory/auth-ui 10.1.254 → 10.1.257

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 (83) hide show
  1. package/dist-client/bootstrap.js +2 -2
  2. package/dist-client/bootstrap.js.map +1 -1
  3. package/dist-client/components/access-decision-popup.d.ts +9 -8
  4. package/dist-client/components/access-decision-popup.js +46 -58
  5. package/dist-client/components/access-decision-popup.js.map +1 -1
  6. package/dist-client/components/access-request-popup.d.ts +13 -8
  7. package/dist-client/components/access-request-popup.js +78 -102
  8. package/dist-client/components/access-request-popup.js.map +1 -1
  9. package/dist-client/components/app-binding-detail.d.ts +7 -5
  10. package/dist-client/components/app-binding-detail.js +62 -82
  11. package/dist-client/components/app-binding-detail.js.map +1 -1
  12. package/dist-client/components/appliance-editor.d.ts +10 -9
  13. package/dist-client/components/appliance-editor.js +87 -138
  14. package/dist-client/components/appliance-editor.js.map +1 -1
  15. package/dist-client/components/application-editor.d.ts +7 -5
  16. package/dist-client/components/application-editor.js +58 -92
  17. package/dist-client/components/application-editor.js.map +1 -1
  18. package/dist-client/components/my-login-history.js +18 -40
  19. package/dist-client/components/my-login-history.js.map +1 -1
  20. package/dist-client/components/my-passkeys.d.ts +4 -3
  21. package/dist-client/components/my-passkeys.js +29 -51
  22. package/dist-client/components/my-passkeys.js.map +1 -1
  23. package/dist-client/components/my-roles.d.ts +3 -4
  24. package/dist-client/components/my-roles.js +36 -55
  25. package/dist-client/components/my-roles.js.map +1 -1
  26. package/dist-client/components/partner-role-editor.js +27 -50
  27. package/dist-client/components/partner-role-editor.js.map +1 -1
  28. package/dist-client/components/profile-component.d.ts +2 -2
  29. package/dist-client/components/profile-component.js +18 -26
  30. package/dist-client/components/profile-component.js.map +1 -1
  31. package/dist-client/components/role-template-picker.d.ts +3 -3
  32. package/dist-client/components/role-template-picker.js +28 -42
  33. package/dist-client/components/role-template-picker.js.map +1 -1
  34. package/dist-client/components/user-detail.d.ts +23 -22
  35. package/dist-client/components/user-detail.js +125 -179
  36. package/dist-client/components/user-detail.js.map +1 -1
  37. package/dist-client/components/user-invite.d.ts +8 -6
  38. package/dist-client/components/user-invite.js +106 -141
  39. package/dist-client/components/user-invite.js.map +1 -1
  40. package/dist-client/components/user-popups-standard.test.js +1 -1
  41. package/dist-client/components/user-popups-standard.test.js.map +1 -1
  42. package/dist-client/components/user-role-editor.js +72 -122
  43. package/dist-client/components/user-role-editor.js.map +1 -1
  44. package/dist-client/entity360/person-360.d.ts +8 -11
  45. package/dist-client/entity360/person-360.js +49 -68
  46. package/dist-client/entity360/person-360.js.map +1 -1
  47. package/dist-client/entries/oauth2/oauth2-decision-page.js +13 -25
  48. package/dist-client/entries/oauth2/oauth2-decision-page.js.map +1 -1
  49. package/dist-client/identity-confirmation/identity-confirmation.d.ts +13 -14
  50. package/dist-client/identity-confirmation/identity-confirmation.js +39 -45
  51. package/dist-client/identity-confirmation/identity-confirmation.js.map +1 -1
  52. package/dist-client/pages/audit/audit-page.d.ts +4 -4
  53. package/dist-client/pages/audit/audit-page.js +28 -39
  54. package/dist-client/pages/audit/audit-page.js.map +1 -1
  55. package/dist-client/pages/domain-link/domain-link-management.js +1 -1
  56. package/dist-client/pages/domain-link/domain-link-management.js.map +1 -1
  57. package/dist-client/pages/partner/partner-management.js +30 -68
  58. package/dist-client/pages/partner/partner-management.js.map +1 -1
  59. package/dist-client/pages/role/role-extra-removal.test.d.mts +1 -0
  60. package/dist-client/pages/role/role-extra-removal.test.mjs +73 -0
  61. package/dist-client/pages/role/role-extra-removal.test.mjs.map +1 -0
  62. package/dist-client/pages/role/role-list-page.js +20 -22
  63. package/dist-client/pages/role/role-list-page.js.map +1 -1
  64. package/dist-client/pages/role/role-privilege-page.d.ts +36 -8
  65. package/dist-client/pages/role/role-privilege-page.js +216 -158
  66. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  67. package/dist-client/pages/role/role-task-review.test.js +20 -16
  68. package/dist-client/pages/role/role-task-review.test.js.map +1 -1
  69. package/dist-client/pages/role/role-template-gap.test.js +8 -3
  70. package/dist-client/pages/role/role-template-gap.test.js.map +1 -1
  71. package/dist-client/tsconfig.tsbuildinfo +1 -1
  72. package/helps/auth/roles.en.md +13 -49
  73. package/helps/auth/roles.ko.md +13 -32
  74. package/package.json +9 -8
  75. package/tests/page-state.test.ts +2 -1
  76. package/tests/password-rule-shown.test.ts +2 -2
  77. package/tests/person-360-sections.test.ts +9 -5
  78. package/translations/en.json +5 -1
  79. package/translations/es.json +5 -1
  80. package/translations/ja.json +5 -1
  81. package/translations/ko.json +5 -1
  82. package/translations/ms.json +5 -1
  83. package/translations/zh.json +5 -1
@@ -6,7 +6,8 @@ import '@operato/input/ox-input-search.js';
6
6
  import gql from 'graphql-tag';
7
7
  import { css, html, nothing } from 'lit';
8
8
  import { customElement, property, state } from 'lit/decorators.js';
9
- import { client, gqlContext } from '@operato/graphql';
9
+ import { live } from 'lit/directives/live.js';
10
+ import { client, gqlContext, MutationController, QueryController } from '@operato/graphql';
10
11
  import { i18next, localize } from '@operato/i18n';
11
12
  import { KNOWN_AXES, axisLabel } from '../../utils/privilege-axis.js';
12
13
  import { openPopup } from '@operato/layout';
@@ -72,7 +73,104 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
72
73
  this.showUnheldTasks = false;
73
74
  /** null until asked for; [] when this person may not read templates (`roleTemplates` asks user:mutation). */
74
75
  this.templates = null;
75
- this.gap = null;
76
+ /**
77
+ * 한 번의 질의로 **전체 권한과 이 역할이 가진 것**을 함께 읽는다(`rolePrivileges(roleId:)`).
78
+ *
79
+ * 역할 이름과 가진 사람은 `roles(filters: id)` 로 같은 문서에서 받는다 — 옛 편집기는 역할을
80
+ * **이름**으로 다시 찾았고, 부모 도메인에 같은 이름이 있으면 남의 역할을 그렸다.
81
+ *
82
+ * A declared read (ADR-0190): another role reads again, and an answer for the role left never lands.
83
+ */
84
+ this.roleRead = new QueryController(this, gql `
85
+ query ($roleId: String!, $filters: [Filter!]) {
86
+ rolePrivileges(roleId: $roleId) {
87
+ id
88
+ name
89
+ category
90
+ description
91
+ assigned
92
+ deprecated
93
+ }
94
+ roles(filters: $filters) {
95
+ items {
96
+ id
97
+ name
98
+ description
99
+ templateId
100
+ domain {
101
+ id
102
+ name
103
+ subdomain
104
+ }
105
+ users {
106
+ id
107
+ name
108
+ email
109
+ }
110
+ taskCoverage {
111
+ tasks {
112
+ taskId
113
+ module
114
+ status
115
+ held { ${GRANT_FIELDS} }
116
+ missing { ${GRANT_FIELDS} }
117
+ undeclared { ${GRANT_FIELDS} }
118
+ }
119
+ unclassified { ${GRANT_FIELDS} }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ `, {
125
+ variables: () => {
126
+ const roleId = this.lifecycle?.resourceId;
127
+ return roleId ? { roleId, filters: [{ name: 'id', operator: 'eq', value: roleId }] } : undefined;
128
+ },
129
+ context: gqlContext()
130
+ });
131
+ /** The role against its base template, read while the comparison is open - again for another role or base. */
132
+ this.gapRead = new QueryController(this, gql `
133
+ query ($filters: [Filter!]) {
134
+ roles(filters: $filters) {
135
+ items {
136
+ id
137
+ templateGap {
138
+ templateId
139
+ templateName
140
+ missing { ${GRANT_FIELDS} }
141
+ missingCount
142
+ extra { ${GRANT_FIELDS} }
143
+ extraCount
144
+ }
145
+ }
146
+ }
147
+ }
148
+ `, {
149
+ variables: () => {
150
+ const roleId = this.shownRoleId;
151
+ return this.view === 'template' && roleId && this.subject.templateId
152
+ ? { filters: [{ name: 'id', operator: 'eq', value: roleId }] }
153
+ : undefined;
154
+ },
155
+ context: gqlContext()
156
+ });
157
+ /** The role's base, saved one change at a time: the choice is locked while one is out, then shows what was saved. */
158
+ this.baseSave = new MutationController(this, gql `
159
+ mutation ($id: String!, $patch: RolePatch!) {
160
+ updateRole(id: $id, patch: $patch) {
161
+ id
162
+ templateId
163
+ }
164
+ }
165
+ `, { context: gqlContext() });
166
+ /** What the role lacks against its base, added once at a time; the role and its comparison read again after. */
167
+ this.templateTopUp = new MutationController(this, gql `
168
+ mutation ($templateId: String!, $name: String) {
169
+ createRoleFromTemplate(templateId: $templateId, name: $name) {
170
+ id
171
+ }
172
+ }
173
+ `, { context: gqlContext(), refetch: () => [this.roleRead, this.gapRead] });
76
174
  }
77
175
  static { this.styles = [
78
176
  /* The frame owns the page shell; the two panes are the content it sizes (ADR-0060 decision 8). */
@@ -615,6 +713,18 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
615
713
  }
616
714
  `
617
715
  ]; }
716
+ /** The role shown, once its read is taken - what every action here acts on. */
717
+ get shownRoleId() {
718
+ return this.fetchState === 'loaded' && this.subject?.id === this.lifecycle?.resourceId ? this.subject.id : undefined;
719
+ }
720
+ /**
721
+ * The comparison against the base the role has now. Another role reads it again (the read's variables change, so the
722
+ * one before is gone); another base does not, so a comparison with the base before is none until it is read again.
723
+ */
724
+ get gap() {
725
+ const gap = this.gapRead.data?.roles?.items?.[0]?.templateGap;
726
+ return gap?.templateId === this.subject.templateId ? gap : null;
727
+ }
618
728
  /**
619
729
  * 부모 도메인의 역할은 여기서 못 고친다 — `updateRole` 의 범위가 `domain.id` 하나다.
620
730
  * 목록이 상속으로 보여 주므로 관리자가 여기까지 온다. 읽기 전용으로 두고 어디로 가야 하는지
@@ -628,8 +738,12 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
628
738
  ownerDomainName() {
629
739
  return this.subject?.domain?.name || this.subject?.domain?.subdomain || '';
630
740
  }
741
+ /**
742
+ * The privileges can be changed: the role is read, is this site's own, and no change of it is out - a save or a
743
+ * top-up from the template reads the role again when it is done, and that read would put back what was typed meanwhile.
744
+ */
631
745
  get savable() {
632
- return this.fetchState === 'loaded' && !this.saving && !this.inherited;
746
+ return this.fetchState === 'loaded' && !this.saving && !this.inherited && !this.templateTopUp.pending;
633
747
  }
634
748
  get visibleModules() {
635
749
  return filterPrivilegeModules(this.modules, this.keyword, moduleLabel);
@@ -852,7 +966,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
852
966
  taskEditButton(task) {
853
967
  if (this.inherited || task.status === 'broken')
854
968
  return nothing;
855
- const disabled = this.dirty || this.saving;
969
+ const disabled = this.dirty || !this.savable;
856
970
  const why = this.dirty ? i18next.t('text.save before editing by task') : '';
857
971
  return html `
858
972
  ${task.status !== 'all'
@@ -865,7 +979,9 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
865
979
  }
866
980
  /** Adds a task's missing privileges to the role, or takes its own off - after showing which - and saves. */
867
981
  async editTask(task, mode) {
868
- if (this.dirty || this.saving || this.inherited)
982
+ const roleId = this.shownRoleId;
983
+ const coverage = this.coverage;
984
+ if (!roleId || this.dirty || !this.savable)
869
985
  return;
870
986
  const name = taskName(task.taskId);
871
987
  const { remove, kept } = mode === 'remove' ? pairsToRemove(task, this.coverage.tasks) : { remove: [], kept: [] };
@@ -888,7 +1004,8 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
888
1004
  confirmButton: { text: i18next.t(mode === 'add' ? 'button.add task' : 'button.remove task') },
889
1005
  cancelButton: { text: i18next.t('button.cancel') }
890
1006
  });
891
- if (!confirmed)
1007
+ /* The task was read for one role: a screen that moved on, or read the role again, is not changed by it. */
1008
+ if (!confirmed || this.shownRoleId !== roleId || this.coverage !== coverage || this.dirty || !this.savable)
892
1009
  return;
893
1010
  const idOf = new Map(this.modules.flatMap(module => module.cells.map(cell => [pairOf(module.category, cell.axis), cell.id])));
894
1011
  const next = new Set(this.assigned);
@@ -930,7 +1047,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
930
1047
  * words (경영지원, 인사급여) is given its base here, once, and the role list counts what it lacks from then on.
931
1048
  * Picking saves the base at once and touches no privilege. The role is never renamed to make a pair: an approval line
932
1049
  * holds the role. What the role holds beyond the template is shown as wider, not wrong — the site may have widened it
933
- * on purpose — and nothing takes it away. What it lacks can be added; adding is the same top-up the template button
1050
+ * on purpose. Extras can be removed after explicit confirmation. What it lacks can be added; adding is the same top-up the template button
934
1051
  * on the role list runs (`createRoleFromTemplate` onto this role's name).
935
1052
  */
936
1053
  renderTemplate() {
@@ -945,7 +1062,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
945
1062
  const gap = this.gap;
946
1063
  return html `
947
1064
  <div review>
948
- <select ?disabled=${this.inherited || this.saving} @change=${(event) => this.setBaseTemplate(event.target.value)}>
1065
+ <select .value=${live(baseId)} ?disabled=${this.inherited || this.saving || this.baseSave.pending} @change=${(event) => this.setBaseTemplate(event.target.value)}>
949
1066
  <option value="" ?selected=${!baseId}>${i18next.t('text.no base template')}</option>
950
1067
  ${this.templates.map(one => html `<option value=${one.id} ?selected=${one.id === baseId}>${one.name}</option>`)}
951
1068
  ${baseId && !template ? html `<option value=${baseId} selected>${baseId}</option>` : nothing}
@@ -961,7 +1078,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
961
1078
  ${i18next.t('label.missing against the template')}
962
1079
  <span count>${gap.missingCount}</span>
963
1080
  ${gap.missingCount && !this.inherited
964
- ? html `<button quiet ?disabled=${this.dirty || this.saving || Boolean(template?.definitionErrors.length)} @click=${() => this.addMissing()}>
1081
+ ? html `<button quiet ?disabled=${this.dirty || !this.savable || Boolean(template?.definitionErrors.length)} @click=${() => this.addMissing()}>
965
1082
  ${i18next.t('button.add')}
966
1083
  </button>`
967
1084
  : nothing}
@@ -970,6 +1087,11 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
970
1087
  <h4>
971
1088
  ${i18next.t('label.wider than the template')}
972
1089
  <span count>${gap.extraCount}</span>
1090
+ ${gap.extraCount && !this.inherited ? html `
1091
+ <button quiet ?disabled=${this.dirty || !this.savable} @click=${() => this.removeExtra()}>
1092
+ ${i18next.t('button.remove extra privileges')}
1093
+ </button>
1094
+ ` : nothing}
973
1095
  </h4>
974
1096
  ${gap.extraCount ? this.renderSentences(gap.extra) : nothing}
975
1097
  `
@@ -1003,70 +1125,58 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1003
1125
  /* Reading templates asks the same door creating a role does; without it there is nothing to compare with. */
1004
1126
  const templates = response.errors?.length ? [] : response.data?.roleTemplates || [];
1005
1127
  this.templates = templates;
1006
- await this.readGap();
1007
1128
  }
1008
1129
  /** Saves the role's base template - `''` bases it on none. The privileges stay as they are. */
1009
1130
  async setBaseTemplate(templateId) {
1010
- if (this.inherited || this.saving)
1131
+ const roleId = this.shownRoleId;
1132
+ if (!roleId || this.inherited || this.saving || this.baseSave.pending)
1011
1133
  return;
1012
- const response = await client.mutate({
1013
- mutation: gql `
1014
- mutation ($id: String!, $patch: RolePatch!) {
1015
- updateRole(id: $id, patch: $patch) {
1016
- id
1017
- templateId
1018
- }
1019
- }
1020
- `,
1021
- variables: { id: this.subject.id, patch: { templateId: templateId || null } },
1022
- context: gqlContext()
1023
- });
1024
- if (response.errors?.length) {
1025
- document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
1026
- this.requestUpdate();
1134
+ const saved = await this.baseSave.run({ id: roleId, patch: { templateId: templateId || null } });
1135
+ /* Saved for the role it was sent for: a screen that moved on to another role keeps that one's base. */
1136
+ if (this.shownRoleId !== roleId)
1027
1137
  return;
1028
- }
1029
- this.subject = { ...this.subject, templateId: response.data?.updateRole?.templateId || null };
1030
- await this.readGap();
1138
+ /* Refused (the link said why): the choice goes back to the base saved. */
1139
+ if (!saved)
1140
+ return this.requestUpdate();
1141
+ this.subject = { ...this.subject, templateId: saved.updateRole?.templateId || null };
1142
+ await this.gapRead.refetch();
1031
1143
  }
1032
- async readGap() {
1033
- this.gap = null;
1034
- const baseId = this.subject?.templateId;
1035
- if (!baseId)
1144
+ /** Remove only the displayed extras, after explicit confirmation for this role and snapshot. */
1145
+ async removeExtra() {
1146
+ const gap = this.gap;
1147
+ if (!gap?.extraCount || !this.active || !this.savable || this.dirty)
1036
1148
  return;
1037
- const response = await client.query({
1038
- query: gql `
1039
- query ($filters: [Filter!]) {
1040
- roles(filters: $filters) {
1041
- items {
1042
- id
1043
- templateGap {
1044
- templateId
1045
- templateName
1046
- missing { ${GRANT_FIELDS} }
1047
- missingCount
1048
- extra { ${GRANT_FIELDS} }
1049
- extraCount
1050
- }
1051
- }
1052
- }
1053
- }
1054
- `,
1055
- variables: { filters: [{ name: 'id', operator: 'eq', value: this.subject.id }] },
1056
- context: gqlContext()
1149
+ const roleId = this.shownRoleId;
1150
+ if (!roleId)
1151
+ return;
1152
+ const assigned = this.assigned;
1153
+ const list = gap.extra.flatMap(grant => grant.axes.map(axis => this.sentenceOf(grant.category, axis))).join(', ');
1154
+ const confirmed = await OxPrompt.open({
1155
+ type: 'warning',
1156
+ title: i18next.t('button.remove extra privileges'),
1157
+ text: i18next.t('text.remove extra privileges', { role: this.subject.name, template: gap.templateName, count: gap.extraCount, list }),
1158
+ confirmButton: { text: i18next.t('button.delete') },
1159
+ cancelButton: { text: i18next.t('button.cancel') }
1057
1160
  });
1058
- if (response.errors?.length) {
1059
- document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
1161
+ // A confirmation for an earlier role, comparison or edit must never change the current screen.
1162
+ if (!confirmed || this.shownRoleId !== roleId || this.gap !== gap || this.assigned !== assigned || !this.active || !this.savable || this.dirty)
1060
1163
  return;
1061
- }
1062
- if (this.subject?.templateId === baseId)
1063
- this.gap = response.data?.roles?.items?.[0]?.templateGap || null;
1164
+ const extras = new Set(gap.extra.flatMap(grant => grant.axes.map(axis => pairOf(grant.category, axis))));
1165
+ const removeIds = new Set(this.modules.flatMap(module => module.cells
1166
+ .filter(cell => extras.has(pairOf(module.category, cell.axis)))
1167
+ .map(cell => cell.id)));
1168
+ this.assigned = new Set([...assigned].filter(id => !removeIds.has(id)));
1169
+ this.dirty = true;
1170
+ await this.onSave();
1171
+ await this.gapRead.refetch();
1064
1172
  }
1065
1173
  /** Adds what the role lacks against the picked template — after showing what it adds. */
1066
1174
  async addMissing() {
1067
1175
  const gap = this.gap;
1068
- if (!gap?.missingCount || this.dirty || this.inherited)
1176
+ const roleId = this.shownRoleId;
1177
+ if (!gap?.missingCount || !roleId || this.dirty || !this.savable)
1069
1178
  return;
1179
+ const roleName = this.subject.name;
1070
1180
  const list = gap.missing.flatMap(grant => grant.axes.map(axis => this.sentenceOf(grant.category, axis))).join(', ');
1071
1181
  const confirmed = await OxPrompt.open({
1072
1182
  type: 'info',
@@ -1075,26 +1185,12 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1075
1185
  confirmButton: { text: i18next.t('button.add') },
1076
1186
  cancelButton: { text: i18next.t('button.cancel') }
1077
1187
  });
1078
- if (!confirmed)
1079
- return;
1080
- const response = await client.mutate({
1081
- mutation: gql `
1082
- mutation ($templateId: String!, $name: String) {
1083
- createRoleFromTemplate(templateId: $templateId, name: $name) {
1084
- id
1085
- }
1086
- }
1087
- `,
1088
- variables: { templateId: gap.templateId, name: this.subject.name },
1089
- context: gqlContext()
1090
- });
1091
- if (response.errors?.length) {
1092
- document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
1188
+ /* Added to the role the comparison was read for, by that comparison - never to a role the screen moved to. */
1189
+ if (!confirmed || this.shownRoleId !== roleId || this.gap !== gap || this.dirty || !this.savable)
1093
1190
  return;
1191
+ if (await this.templateTopUp.run({ templateId: gap.templateId, name: roleName })) {
1192
+ document.dispatchEvent(new CustomEvent('notify', { detail: { message: i18next.t('text.data_updated_successfully') } }));
1094
1193
  }
1095
- document.dispatchEvent(new CustomEvent('notify', { detail: { message: i18next.t('text.data_updated_successfully') } }));
1096
- await this.fetch();
1097
- await this.readGap();
1098
1194
  }
1099
1195
  /**
1100
1196
  * 한 줄 — 문장과 [떼기].
@@ -1113,7 +1209,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1113
1209
 
1114
1210
  ${this.inherited
1115
1211
  ? nothing
1116
- : html `<button drop title=${i18next.t('button.drop')} @click=${() => this.onToggle(cell.id, false)}>
1212
+ : html `<button drop ?disabled=${!this.savable} title=${i18next.t('button.drop')} @click=${() => this.onToggle(cell.id, false)}>
1117
1213
  <md-icon>close</md-icon>
1118
1214
  </button>`}
1119
1215
  </li>
@@ -1273,14 +1369,14 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1273
1369
  return html `
1274
1370
  <li
1275
1371
  ?ghost=${cell.deprecated}
1276
- ?readonly=${this.inherited || (cell.deprecated && !on)}
1372
+ ?readonly=${!this.savable || (cell.deprecated && !on)}
1277
1373
  @click=${(event) => this.toggleCellFromRow(event, cell, on)}
1278
1374
  >
1279
1375
  <input
1280
1376
  type="checkbox"
1281
1377
  id="cell-${cell.id}"
1282
1378
  .checked=${on}
1283
- ?disabled=${this.inherited || (cell.deprecated && !on)}
1379
+ ?disabled=${!this.savable || (cell.deprecated && !on)}
1284
1380
  @change=${(e) => this.onToggle(cell.id, e.target.checked)}
1285
1381
  />
1286
1382
  <label for="cell-${cell.id}">${axisLabel(cell.axis)}</label>
@@ -1298,80 +1394,44 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1298
1394
  cellCount() {
1299
1395
  return this.modules.reduce((sum, module) => sum + module.cells.length, 0);
1300
1396
  }
1397
+ /* The shell sets `lifecycle`; the role read follows it. A late call for an earlier address never sets it back. */
1301
1398
  async pageUpdated(changes, lifecycle) {
1302
1399
  if (this.active) {
1303
- this.lifecycle = lifecycle;
1304
- await this.fetch();
1400
+ this.mySubdomain = (await getDomain())?.subdomain || '';
1401
+ if (this.active && this.lifecycle?.resourceId === lifecycle.resourceId && lifecycle.params?.view === 'template') {
1402
+ await this.showView('template');
1403
+ }
1305
1404
  }
1306
1405
  }
1406
+ /** Reads the role's privileges again - the save just wrote them, or the read failed and the person asks again. */
1407
+ async fetch() {
1408
+ await this.roleRead.refetch();
1409
+ }
1307
1410
  /**
1308
- * 한 번의 질의로 **전체 권한과 이 역할이 가진 것**을 함께 읽는다(`rolePrivileges(roleId:)`).
1309
- *
1310
- * 역할 이름과 가진 사람은 `roles(filters: id)` 로 같은 문서에서 받는다 — 옛 편집기는 역할을
1311
- * **이름**으로 다시 찾았고, 부모 도메인에 같은 이름이 있으면 남의 역할을 그렸다.
1411
+ * What the role read says, taken into the page before it draws (ADR-0190): loading, failed - a thrown read too - or
1412
+ * the role, whose privileges become the page's edits once per answer.
1312
1413
  */
1313
- async fetch() {
1314
- const roleId = this.lifecycle?.resourceId;
1315
- if (!roleId) {
1414
+ willUpdate() {
1415
+ const read = this.roleRead;
1416
+ if (!this.lifecycle)
1417
+ return;
1418
+ if (!this.lifecycle.resourceId) {
1316
1419
  this.fetchState = 'failed';
1317
1420
  this.loadError = new Error(i18next.t('text.no role to show'));
1318
1421
  return;
1319
1422
  }
1320
- this.fetchState = 'loading';
1321
- this.loadError = null;
1322
- this.mySubdomain = (await getDomain())?.subdomain || '';
1323
- const response = await client.query({
1324
- query: gql `
1325
- query ($roleId: String!, $filters: [Filter!]) {
1326
- rolePrivileges(roleId: $roleId) {
1327
- id
1328
- name
1329
- category
1330
- description
1331
- assigned
1332
- deprecated
1333
- }
1334
- roles(filters: $filters) {
1335
- items {
1336
- id
1337
- name
1338
- description
1339
- templateId
1340
- domain {
1341
- id
1342
- name
1343
- subdomain
1344
- }
1345
- users {
1346
- id
1347
- name
1348
- email
1349
- }
1350
- taskCoverage {
1351
- tasks {
1352
- taskId
1353
- module
1354
- status
1355
- held { ${GRANT_FIELDS} }
1356
- missing { ${GRANT_FIELDS} }
1357
- undeclared { ${GRANT_FIELDS} }
1358
- }
1359
- unclassified { ${GRANT_FIELDS} }
1360
- }
1361
- }
1362
- }
1363
- }
1364
- `,
1365
- variables: { roleId, filters: [{ name: 'id', operator: 'eq', value: roleId }] },
1366
- context: gqlContext()
1367
- });
1368
- if (response.errors?.length) {
1423
+ if (read.error) {
1369
1424
  /* 실패를 「권한이 하나도 없다」로 그리지 않는다 — 저장하면 실제로 그렇게 된다. */
1370
1425
  this.fetchState = 'failed';
1371
- this.loadError = response.errors;
1426
+ this.loadError = read.error;
1427
+ return;
1428
+ }
1429
+ if (read.loading || !read.data) {
1430
+ this.fetchState = 'loading';
1431
+ this.loadError = null;
1372
1432
  return;
1373
1433
  }
1374
- const role = response.data?.roles?.items?.[0];
1434
+ const role = read.data.roles?.items?.[0];
1375
1435
  /*
1376
1436
  * A role id nobody has resolves with no error and no role, so the screen drew an empty
1377
1437
  * privilege list - a role that is gone looked exactly like a role that has been granted
@@ -1382,17 +1442,15 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1382
1442
  this.loadError = new Error(i18next.t('text.this role no longer exists'));
1383
1443
  return;
1384
1444
  }
1385
- this.subject = role;
1386
- this.holders = this.subject.users || [];
1387
- this.coverage = role.taskCoverage || { tasks: [], unclassified: [] };
1388
- this.build(response.data?.rolePrivileges || []);
1445
+ if (read.data !== this.taken) {
1446
+ this.taken = read.data;
1447
+ this.subject = role;
1448
+ this.holders = role.users || [];
1449
+ this.coverage = role.taskCoverage || { tasks: [], unclassified: [] };
1450
+ this.build(read.data.rolePrivileges || []);
1451
+ }
1389
1452
  this.fetchState = 'loaded';
1390
- /*
1391
- * The shell reads `context` once, when the page becomes active - before this fetch
1392
- * resolves. Without this call the header keeps the fallback, so the screen showed
1393
- * "권한" and never said which role was being edited.
1394
- */
1395
- this.updateContext();
1453
+ this.loadError = null;
1396
1454
  }
1397
1455
  /** 평평한 목록을 모듈로 접는다. 축 순서는 아는 것 먼저, 나머지는 이름 순. */
1398
1456
  build(privileges) {
@@ -1422,7 +1480,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1422
1480
  }
1423
1481
  }
1424
1482
  onToggle(id, checked) {
1425
- if (this.assigned.has(id) === checked) {
1483
+ if (!this.savable || this.assigned.has(id) === checked) {
1426
1484
  return;
1427
1485
  }
1428
1486
  const next = new Set(this.assigned);
@@ -1435,7 +1493,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1435
1493
  }
1436
1494
  /** 라벨·설명을 포함한 행 전체를 누를 수 있게 한다. checkbox와 키보드 Space는 native change가 맡는다. */
1437
1495
  toggleCellFromRow(event, cell, on) {
1438
- if (this.inherited || (cell.deprecated && !on) || event.target instanceof HTMLInputElement) {
1496
+ if (!this.savable || (cell.deprecated && !on) || event.target instanceof HTMLInputElement) {
1439
1497
  return;
1440
1498
  }
1441
1499
  /* label의 기본 동작이 checkbox change를 한 번 더 보내지 않게 막는다. */
@@ -1462,7 +1520,8 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1462
1520
  * `privileges` 만 보낸다 — 이름과 설명은 목록 격자가 소유한다.
1463
1521
  */
1464
1522
  async onSave() {
1465
- if (!this.savable) {
1523
+ const roleId = this.shownRoleId;
1524
+ if (!roleId || !this.savable) {
1466
1525
  return;
1467
1526
  }
1468
1527
  this.saving = true;
@@ -1477,7 +1536,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1477
1536
  }
1478
1537
  `,
1479
1538
  variables: {
1480
- id: this.subject.id,
1539
+ id: roleId,
1481
1540
  patch: { privileges: [...this.assigned].map(id => ({ id })) }
1482
1541
  },
1483
1542
  context: gqlContext()
@@ -1487,6 +1546,9 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
1487
1546
  return;
1488
1547
  }
1489
1548
  document.dispatchEvent(new CustomEvent('notify', { detail: { message: i18next.t('text.data_updated_successfully') } }));
1549
+ /* Saved while the screen moved on: what is edited there now is another role's, and stays. */
1550
+ if (this.shownRoleId !== roleId)
1551
+ return;
1490
1552
  /* 유령을 뗐으면 목록에서 사라져야 하고, 켠 것이 요약에 반영돼야 한다. */
1491
1553
  await this.fetch();
1492
1554
  this.dirty = false;
@@ -1561,10 +1623,6 @@ __decorate([
1561
1623
  state(),
1562
1624
  __metadata("design:type", Object)
1563
1625
  ], RolePrivilegePage.prototype, "templates", void 0);
1564
- __decorate([
1565
- state(),
1566
- __metadata("design:type", Object)
1567
- ], RolePrivilegePage.prototype, "gap", void 0);
1568
1626
  RolePrivilegePage = __decorate([
1569
1627
  customElement('role-privilege-page')
1570
1628
  ], RolePrivilegePage);