@things-factory/auth-ui 10.1.256 → 10.1.258
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/bootstrap.js +2 -2
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/access-decision-popup.d.ts +9 -8
- package/dist-client/components/access-decision-popup.js +46 -58
- package/dist-client/components/access-decision-popup.js.map +1 -1
- package/dist-client/components/access-request-popup.d.ts +13 -8
- package/dist-client/components/access-request-popup.js +78 -102
- package/dist-client/components/access-request-popup.js.map +1 -1
- package/dist-client/components/app-binding-detail.d.ts +7 -5
- package/dist-client/components/app-binding-detail.js +62 -82
- package/dist-client/components/app-binding-detail.js.map +1 -1
- package/dist-client/components/appliance-editor.d.ts +10 -9
- package/dist-client/components/appliance-editor.js +87 -138
- package/dist-client/components/appliance-editor.js.map +1 -1
- package/dist-client/components/application-editor.d.ts +7 -5
- package/dist-client/components/application-editor.js +58 -92
- package/dist-client/components/application-editor.js.map +1 -1
- package/dist-client/components/my-login-history.js +18 -40
- package/dist-client/components/my-login-history.js.map +1 -1
- package/dist-client/components/my-passkeys.d.ts +4 -3
- package/dist-client/components/my-passkeys.js +29 -51
- package/dist-client/components/my-passkeys.js.map +1 -1
- package/dist-client/components/my-roles.d.ts +3 -4
- package/dist-client/components/my-roles.js +36 -55
- package/dist-client/components/my-roles.js.map +1 -1
- package/dist-client/components/partner-role-editor.js +27 -50
- package/dist-client/components/partner-role-editor.js.map +1 -1
- package/dist-client/components/profile-component.d.ts +2 -2
- package/dist-client/components/profile-component.js +18 -26
- package/dist-client/components/profile-component.js.map +1 -1
- package/dist-client/components/role-template-picker.d.ts +3 -3
- package/dist-client/components/role-template-picker.js +28 -42
- package/dist-client/components/role-template-picker.js.map +1 -1
- package/dist-client/components/user-detail.d.ts +23 -22
- package/dist-client/components/user-detail.js +125 -179
- package/dist-client/components/user-detail.js.map +1 -1
- package/dist-client/components/user-invite.d.ts +8 -6
- package/dist-client/components/user-invite.js +106 -141
- package/dist-client/components/user-invite.js.map +1 -1
- package/dist-client/components/user-popups-standard.test.js +1 -1
- package/dist-client/components/user-popups-standard.test.js.map +1 -1
- package/dist-client/components/user-role-editor.js +72 -122
- package/dist-client/components/user-role-editor.js.map +1 -1
- package/dist-client/entity360/person-360.d.ts +8 -11
- package/dist-client/entity360/person-360.js +49 -68
- package/dist-client/entity360/person-360.js.map +1 -1
- package/dist-client/entries/oauth2/oauth2-decision-page.js +13 -25
- package/dist-client/entries/oauth2/oauth2-decision-page.js.map +1 -1
- package/dist-client/identity-confirmation/identity-confirmation.d.ts +13 -14
- package/dist-client/identity-confirmation/identity-confirmation.js +39 -45
- package/dist-client/identity-confirmation/identity-confirmation.js.map +1 -1
- package/dist-client/pages/audit/audit-page.d.ts +4 -4
- package/dist-client/pages/audit/audit-page.js +28 -39
- package/dist-client/pages/audit/audit-page.js.map +1 -1
- package/dist-client/pages/domain-link/domain-link-management.js +1 -1
- package/dist-client/pages/domain-link/domain-link-management.js.map +1 -1
- package/dist-client/pages/partner/partner-management.js +30 -68
- package/dist-client/pages/partner/partner-management.js.map +1 -1
- package/dist-client/pages/role/role-extra-removal.test.d.mts +1 -0
- package/dist-client/pages/role/role-extra-removal.test.mjs +73 -0
- package/dist-client/pages/role/role-extra-removal.test.mjs.map +1 -0
- package/dist-client/pages/role/role-list-page.js +20 -22
- package/dist-client/pages/role/role-list-page.js.map +1 -1
- package/dist-client/pages/role/role-privilege-page.d.ts +36 -8
- package/dist-client/pages/role/role-privilege-page.js +216 -158
- package/dist-client/pages/role/role-privilege-page.js.map +1 -1
- package/dist-client/pages/role/role-task-review.test.js +20 -16
- package/dist-client/pages/role/role-task-review.test.js.map +1 -1
- package/dist-client/pages/role/role-template-gap.test.js +8 -3
- package/dist-client/pages/role/role-template-gap.test.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/helps/auth/roles.en.md +13 -49
- package/helps/auth/roles.ko.md +13 -32
- package/package.json +11 -10
- package/tests/page-state.test.ts +2 -1
- package/tests/password-rule-shown.test.ts +2 -2
- package/tests/person-360-sections.test.ts +9 -5
- package/translations/en.json +5 -1
- package/translations/es.json +5 -1
- package/translations/ja.json +5 -1
- package/translations/ko.json +5 -1
- package/translations/ms.json +5 -1
- 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 {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
1131
|
+
const roleId = this.shownRoleId;
|
|
1132
|
+
if (!roleId || this.inherited || this.saving || this.baseSave.pending)
|
|
1011
1133
|
return;
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
if (!
|
|
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
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1304
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1314
|
-
const
|
|
1315
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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);
|