@things-factory/auth-ui 10.1.194 → 10.1.202
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 +4 -1
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/access-decision-popup.js +7 -5
- package/dist-client/components/access-decision-popup.js.map +1 -1
- package/dist-client/components/access-request-popup.d.ts +9 -1
- package/dist-client/components/access-request-popup.js +55 -4
- package/dist-client/components/access-request-popup.js.map +1 -1
- package/dist-client/components/my-login-history.js +14 -10
- package/dist-client/components/my-login-history.js.map +1 -1
- package/dist-client/components/role-task-edit.d.ts +22 -0
- package/dist-client/components/role-task-edit.js +17 -0
- package/dist-client/components/role-task-edit.js.map +1 -0
- package/dist-client/components/role-task-edit.test.d.ts +1 -0
- package/dist-client/components/role-task-edit.test.js +34 -0
- package/dist-client/components/role-task-edit.test.js.map +1 -0
- package/dist-client/components/role-template-picker.d.ts +5 -0
- package/dist-client/components/role-template-picker.js +41 -5
- package/dist-client/components/role-template-picker.js.map +1 -1
- package/dist-client/components/user-status-renderer.d.ts +9 -0
- package/dist-client/components/user-status-renderer.js +36 -0
- package/dist-client/components/user-status-renderer.js.map +1 -0
- package/dist-client/entity360/person-360.d.ts +13 -0
- package/dist-client/entity360/person-360.js +96 -0
- package/dist-client/entity360/person-360.js.map +1 -1
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +14 -1
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/access-request/access-requests.js +4 -2
- package/dist-client/pages/access-request/access-requests.js.map +1 -1
- package/dist-client/pages/access-request/my-access-requests.js +3 -2
- package/dist-client/pages/access-request/my-access-requests.js.map +1 -1
- package/dist-client/pages/audit/audit-page.d.ts +46 -0
- package/dist-client/pages/audit/audit-page.js +417 -0
- package/dist-client/pages/audit/audit-page.js.map +1 -0
- package/dist-client/pages/role/role-privilege-page.d.ts +46 -0
- package/dist-client/pages/role/role-privilege-page.js +500 -5
- package/dist-client/pages/role/role-privilege-page.js.map +1 -1
- package/dist-client/pages/role/role-task-review.test.d.ts +1 -0
- package/dist-client/pages/role/role-task-review.test.js +60 -0
- package/dist-client/pages/role/role-task-review.test.js.map +1 -0
- package/dist-client/pages/user/user-list-page.js +1 -31
- package/dist-client/pages/user/user-list-page.js.map +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/access-request.d.ts +15 -1
- package/dist-client/utils/access-request.js +19 -2
- package/dist-client/utils/access-request.js.map +1 -1
- package/dist-client/utils/access-request.test.js +26 -1
- package/dist-client/utils/access-request.test.js.map +1 -1
- package/dist-client/utils/audit-words.d.ts +30 -0
- package/dist-client/utils/audit-words.js +57 -0
- package/dist-client/utils/audit-words.js.map +1 -0
- package/dist-client/utils/audit-words.test.d.ts +1 -0
- package/dist-client/utils/audit-words.test.js +47 -0
- package/dist-client/utils/audit-words.test.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/things-factory.config.js +1 -0
- package/translations/en.json +78 -1
- package/translations/es.json +78 -1
- package/translations/ja.json +78 -1
- package/translations/ko.json +78 -1
- package/translations/ms.json +78 -1
- package/translations/zh.json +78 -1
|
@@ -9,10 +9,13 @@ import { client, gqlContext } from '@operato/graphql';
|
|
|
9
9
|
import { i18next, localize } from '@operato/i18n';
|
|
10
10
|
import { KNOWN_AXES, axisLabel } from '../../utils/privilege-axis.js';
|
|
11
11
|
import { openPopup } from '@operato/layout';
|
|
12
|
+
import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
12
13
|
import { PageView } from '@operato/shell';
|
|
13
14
|
import { CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
|
|
14
15
|
import { getDomain } from '@things-factory/auth-base/dist-client';
|
|
15
16
|
import { filterPrivilegeModules, selectedVisibleModule } from '../../components/role-privilege-picker-state.js';
|
|
17
|
+
import { pairOf, pairsToAdd, pairsToRemove } from '../../components/role-task-edit.js';
|
|
18
|
+
const GRANT_FIELDS = 'category axes';
|
|
16
19
|
/**
|
|
17
20
|
* 한 역할이 무엇을 할 수 있나 — **켜진 것이 문장으로 본문에 선다.**
|
|
18
21
|
*
|
|
@@ -61,6 +64,14 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
61
64
|
this.loadError = null;
|
|
62
65
|
this.saving = false;
|
|
63
66
|
this.dirty = false;
|
|
67
|
+
this.view = 'privileges';
|
|
68
|
+
/** The role laid against every task - computed by the server from the saved privileges (`Role.taskCoverage`). */
|
|
69
|
+
this.coverage = { tasks: [], unclassified: [] };
|
|
70
|
+
this.showUnheldTasks = false;
|
|
71
|
+
/** null until asked for; [] when this person may not read templates (`roleTemplates` asks user:mutation). */
|
|
72
|
+
this.templates = null;
|
|
73
|
+
this.templateId = '';
|
|
74
|
+
this.gap = null;
|
|
64
75
|
}
|
|
65
76
|
static { this.styles = [
|
|
66
77
|
/* The frame owns the page shell; the two panes are the content it sizes (ADR-0060 decision 8). */
|
|
@@ -447,6 +458,157 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
447
458
|
color: var(--md-sys-color-on-surface-variant);
|
|
448
459
|
font-size: 0.85rem;
|
|
449
460
|
}
|
|
461
|
+
|
|
462
|
+
/*
|
|
463
|
+
The three readings of one role - quiet text buttons in the panel's header line, the chosen one underlined.
|
|
464
|
+
They change what the panel shows, not what the role holds, so they are not boxed like an action.
|
|
465
|
+
*/
|
|
466
|
+
[granted] > header nav {
|
|
467
|
+
margin-left: auto;
|
|
468
|
+
display: flex;
|
|
469
|
+
gap: 2px;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
[granted] > header nav button {
|
|
473
|
+
padding: 2px 8px;
|
|
474
|
+
border: none;
|
|
475
|
+
border-bottom: 2px solid transparent;
|
|
476
|
+
background: none;
|
|
477
|
+
font: inherit;
|
|
478
|
+
font-size: 0.8rem;
|
|
479
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
480
|
+
cursor: pointer;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
[granted] > header nav button[selected] {
|
|
484
|
+
border-bottom-color: var(--md-sys-color-primary);
|
|
485
|
+
color: var(--md-sys-color-on-surface);
|
|
486
|
+
font-weight: 600;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
[review] {
|
|
490
|
+
flex: 1;
|
|
491
|
+
min-height: 0;
|
|
492
|
+
overflow: auto;
|
|
493
|
+
padding: 0 12px 12px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
[review] h4 {
|
|
497
|
+
display: flex;
|
|
498
|
+
align-items: baseline;
|
|
499
|
+
gap: 8px;
|
|
500
|
+
margin: 14px 0 4px;
|
|
501
|
+
font-size: 0.85rem;
|
|
502
|
+
font-weight: 600;
|
|
503
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
[review] h4 [count] {
|
|
507
|
+
font-weight: 400;
|
|
508
|
+
color: var(--md-sys-color-outline);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
ul[tasks] {
|
|
512
|
+
list-style: none;
|
|
513
|
+
margin: 0;
|
|
514
|
+
padding: 0;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
ul[tasks] > li {
|
|
518
|
+
padding: 9px 2px;
|
|
519
|
+
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
ul[tasks] > li:last-child {
|
|
523
|
+
border-bottom: none;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
ul[tasks] [task-name] {
|
|
527
|
+
font-size: 0.95rem;
|
|
528
|
+
font-weight: 600;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
ul[tasks] [task-says],
|
|
532
|
+
ul[tasks] [task-missing] {
|
|
533
|
+
margin-top: 2px;
|
|
534
|
+
font-size: 0.85rem;
|
|
535
|
+
line-height: 1.55;
|
|
536
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
ul[tasks] [task-missing] {
|
|
540
|
+
color: var(--md-sys-color-outline);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
ul[tasks][quiet] > li {
|
|
544
|
+
padding: 5px 2px;
|
|
545
|
+
font-size: 0.88rem;
|
|
546
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
[state] {
|
|
550
|
+
margin-left: 8px;
|
|
551
|
+
padding: 0 6px;
|
|
552
|
+
border-radius: var(--md-sys-shape-corner-full, 999px);
|
|
553
|
+
font-size: 0.75rem;
|
|
554
|
+
font-weight: 400;
|
|
555
|
+
background-color: var(--md-sys-color-surface-container-highest);
|
|
556
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
[state][all] {
|
|
560
|
+
background-color: var(--md-sys-color-secondary-container);
|
|
561
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
[state][broken] {
|
|
565
|
+
background-color: var(--md-sys-color-error-container);
|
|
566
|
+
color: var(--md-sys-color-on-error-container);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
ul[sentences] {
|
|
570
|
+
list-style: none;
|
|
571
|
+
margin: 0;
|
|
572
|
+
padding: 0;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
ul[sentences] li {
|
|
576
|
+
padding: 5px 2px;
|
|
577
|
+
font-size: 0.9rem;
|
|
578
|
+
line-height: 1.5;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
button[quiet] {
|
|
582
|
+
padding: 4px 0;
|
|
583
|
+
border: none;
|
|
584
|
+
background: none;
|
|
585
|
+
font: inherit;
|
|
586
|
+
font-size: 0.85rem;
|
|
587
|
+
color: var(--md-sys-color-primary);
|
|
588
|
+
cursor: pointer;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
button[quiet]:disabled {
|
|
592
|
+
color: var(--md-sys-color-outline);
|
|
593
|
+
cursor: default;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
[review] select {
|
|
597
|
+
margin-top: 10px;
|
|
598
|
+
padding: 4px 6px;
|
|
599
|
+
font: inherit;
|
|
600
|
+
font-size: 0.88rem;
|
|
601
|
+
color: var(--md-sys-color-on-surface);
|
|
602
|
+
background-color: var(--md-sys-color-surface);
|
|
603
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
604
|
+
border-radius: var(--md-sys-shape-corner-small, 6px);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
[review] [stale] {
|
|
608
|
+
margin-top: 10px;
|
|
609
|
+
font-size: 0.82rem;
|
|
610
|
+
color: var(--md-sys-color-outline);
|
|
611
|
+
}
|
|
450
612
|
`
|
|
451
613
|
]; }
|
|
452
614
|
/**
|
|
@@ -600,17 +762,304 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
600
762
|
<header>
|
|
601
763
|
${i18next.t('field.granted privileges')}
|
|
602
764
|
<span count>${granted.length}</span>
|
|
765
|
+
<nav>
|
|
766
|
+
${this.viewButton('privileges', i18next.t('label.privileges view'))} ${this.viewButton('tasks', i18next.t('label.tasks view'))}
|
|
767
|
+
${this.viewButton('template', i18next.t('label.template view'))}
|
|
768
|
+
</nav>
|
|
603
769
|
</header>
|
|
604
770
|
|
|
605
|
-
${
|
|
606
|
-
?
|
|
607
|
-
|
|
771
|
+
${this.view === 'tasks'
|
|
772
|
+
? this.renderTasks()
|
|
773
|
+
: this.view === 'template'
|
|
774
|
+
? this.renderTemplate()
|
|
775
|
+
: html `${granted.length
|
|
776
|
+
? html `<ul lines>
|
|
777
|
+
${granted.map(({ category, cell }) => this.renderGrantedLine(category, cell))}
|
|
778
|
+
</ul>`
|
|
779
|
+
: html `<div empty>${i18next.t('text.nothing is on for this role yet')}</div>`}
|
|
780
|
+
${this.renderAdd()}`}
|
|
781
|
+
</div>
|
|
782
|
+
`;
|
|
783
|
+
}
|
|
784
|
+
viewButton(view, label) {
|
|
785
|
+
return html `<button ?selected=${this.view === view} @click=${() => this.showView(view)}>${label}</button>`;
|
|
786
|
+
}
|
|
787
|
+
async showView(view) {
|
|
788
|
+
this.view = view;
|
|
789
|
+
if (view === 'template' && this.templates === null) {
|
|
790
|
+
await this.fetchTemplates();
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* **The role by task** (ADR-0139 decision 11) — which of each task's privileges this role holds, as the server
|
|
795
|
+
* computes it from the saved privileges.
|
|
796
|
+
*
|
|
797
|
+
* It says what the role holds, nothing more: 「전부」 is not "this person can do the work" (another role, the
|
|
798
|
+
* data's own scope or an approval line decide that too), and 「일부」 is not a fault — a privilege several tasks
|
|
799
|
+
* share makes each of them 「일부」.
|
|
800
|
+
*
|
|
801
|
+
* A task can be added to the role or taken off it (the user, 2026-10-20). The role stores privileges, never tasks,
|
|
802
|
+
* so either is a set of privileges, shown before it is saved. Taking a task off keeps what another task the role
|
|
803
|
+
* holds whole still uses (`role-task-edit.ts`). Both save at once and read the coverage again, so they are not
|
|
804
|
+
* offered over unsaved changes - the coverage is the saved role's.
|
|
805
|
+
*/
|
|
806
|
+
renderTasks() {
|
|
807
|
+
const tasks = this.coverage.tasks;
|
|
808
|
+
const held = tasks.filter(task => task.status !== 'none').sort((a, b) => statusOrder(a.status) - statusOrder(b.status));
|
|
809
|
+
const unheld = tasks.filter(task => task.status === 'none');
|
|
810
|
+
return html `
|
|
811
|
+
<div review>
|
|
812
|
+
${this.dirty ? html `<div stale>${i18next.t('text.counted from the saved privileges')}</div>` : nothing}
|
|
813
|
+
${held.length
|
|
814
|
+
? html `<ul tasks>
|
|
815
|
+
${held.map(task => this.renderTask(task))}
|
|
608
816
|
</ul>`
|
|
609
|
-
: html `<div empty>${i18next.t('text.
|
|
610
|
-
${
|
|
817
|
+
: html `<div empty>${i18next.t('text.this role holds no task')}</div>`}
|
|
818
|
+
${unheld.length
|
|
819
|
+
? html `<h4>
|
|
820
|
+
${i18next.t('label.tasks this role does not hold')}
|
|
821
|
+
<span count>${unheld.length}</span>
|
|
822
|
+
<button quiet @click=${() => (this.showUnheldTasks = !this.showUnheldTasks)}>
|
|
823
|
+
${this.showUnheldTasks ? i18next.t('button.fold') : i18next.t('button.unfold')}
|
|
824
|
+
</button>
|
|
825
|
+
</h4>
|
|
826
|
+
${this.showUnheldTasks
|
|
827
|
+
? html `<ul tasks quiet>
|
|
828
|
+
${unheld.map(task => html `<li>${taskName(task.taskId)} ${this.taskEditButton(task)}</li>`)}
|
|
829
|
+
</ul>`
|
|
830
|
+
: nothing}`
|
|
831
|
+
: nothing}
|
|
832
|
+
${this.coverage.unclassified.length
|
|
833
|
+
? html `<h4>
|
|
834
|
+
${i18next.t('label.privileges no task names')}
|
|
835
|
+
<span count>${countOf(this.coverage.unclassified)}</span>
|
|
836
|
+
</h4>
|
|
837
|
+
${this.renderSentences(this.coverage.unclassified)}`
|
|
838
|
+
: nothing}
|
|
839
|
+
</div>
|
|
840
|
+
`;
|
|
841
|
+
}
|
|
842
|
+
/** 더하기 for a task the role lacks some or all of, 빼기 for one it holds some or all of. */
|
|
843
|
+
taskEditButton(task) {
|
|
844
|
+
if (this.inherited || task.status === 'broken')
|
|
845
|
+
return nothing;
|
|
846
|
+
const disabled = this.dirty || this.saving;
|
|
847
|
+
const why = this.dirty ? i18next.t('text.save before editing by task') : '';
|
|
848
|
+
return html `
|
|
849
|
+
${task.status !== 'all'
|
|
850
|
+
? html `<button quiet ?disabled=${disabled} title=${why} @click=${() => this.editTask(task, 'add')}>${i18next.t('button.add task')}</button>`
|
|
851
|
+
: nothing}
|
|
852
|
+
${task.status !== 'none'
|
|
853
|
+
? html `<button quiet ?disabled=${disabled} title=${why} @click=${() => this.editTask(task, 'remove')}>${i18next.t('button.remove task')}</button>`
|
|
854
|
+
: nothing}
|
|
855
|
+
`;
|
|
856
|
+
}
|
|
857
|
+
/** Adds a task's missing privileges to the role, or takes its own off - after showing which - and saves. */
|
|
858
|
+
async editTask(task, mode) {
|
|
859
|
+
if (this.dirty || this.saving || this.inherited)
|
|
860
|
+
return;
|
|
861
|
+
const name = taskName(task.taskId);
|
|
862
|
+
const { remove, kept } = mode === 'remove' ? pairsToRemove(task, this.coverage.tasks) : { remove: [], kept: [] };
|
|
863
|
+
const pairs = mode === 'add' ? pairsToAdd(task) : remove;
|
|
864
|
+
const words = (list) => list.map(pair => this.sentenceOf(pair.split(':')[0], pair.split(':')[1])).join(', ');
|
|
865
|
+
const keptLine = kept.length ? ` ${i18next.t('text.kept for other tasks', { list: words(kept) })}` : '';
|
|
866
|
+
if (!pairs.length) {
|
|
867
|
+
await OxPrompt.open({
|
|
868
|
+
type: 'info',
|
|
869
|
+
title: i18next.t('title.remove task'),
|
|
870
|
+
text: i18next.t('text.nothing to remove for task', { task: name }) + keptLine,
|
|
871
|
+
confirmButton: { text: i18next.t('button.confirm') }
|
|
872
|
+
});
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const confirmed = await OxPrompt.open({
|
|
876
|
+
type: 'info',
|
|
877
|
+
title: i18next.t(mode === 'add' ? 'title.add task' : 'title.remove task'),
|
|
878
|
+
text: i18next.t(mode === 'add' ? 'text.add task privileges' : 'text.remove task privileges', { task: name, count: pairs.length, list: words(pairs) }) + keptLine,
|
|
879
|
+
confirmButton: { text: i18next.t(mode === 'add' ? 'button.add task' : 'button.remove task') },
|
|
880
|
+
cancelButton: { text: i18next.t('button.cancel') }
|
|
881
|
+
});
|
|
882
|
+
if (!confirmed)
|
|
883
|
+
return;
|
|
884
|
+
const idOf = new Map(this.modules.flatMap(module => module.cells.map(cell => [pairOf(module.category, cell.axis), cell.id])));
|
|
885
|
+
const next = new Set(this.assigned);
|
|
886
|
+
for (const pair of pairs) {
|
|
887
|
+
const id = idOf.get(pair);
|
|
888
|
+
if (id)
|
|
889
|
+
mode === 'add' ? next.add(id) : next.delete(id);
|
|
890
|
+
}
|
|
891
|
+
this.assigned = next;
|
|
892
|
+
this.dirty = true;
|
|
893
|
+
await this.onSave();
|
|
894
|
+
}
|
|
895
|
+
renderTask(task) {
|
|
896
|
+
const held = countOf(task.held);
|
|
897
|
+
const total = held + countOf(task.missing);
|
|
898
|
+
const says = i18next.t(`task.${task.taskId}.description`, { defaultValue: '' });
|
|
899
|
+
return html `
|
|
900
|
+
<li>
|
|
901
|
+
<div>
|
|
902
|
+
<span task-name>${taskName(task.taskId)}</span>
|
|
903
|
+
${task.status === 'broken'
|
|
904
|
+
? html `<span state broken>${i18next.t('text.task names an undeclared privilege')}</span>`
|
|
905
|
+
: task.status === 'all'
|
|
906
|
+
? html `<span state all>${i18next.t('text.task held whole')}</span>`
|
|
907
|
+
: html `<span state>${i18next.t('text.task held in part', { held, total })}</span>`}
|
|
908
|
+
${this.taskEditButton(task)}
|
|
909
|
+
</div>
|
|
910
|
+
${says ? html `<div task-says>${says}</div>` : nothing}
|
|
911
|
+
${task.status === 'some'
|
|
912
|
+
? html `<div task-missing>${i18next.t('text.not held x', { x: task.missing.flatMap(grant => grant.axes.map(axis => this.sentenceOf(grant.category, axis))).join(' · ') })}</div>`
|
|
913
|
+
: nothing}
|
|
914
|
+
</li>
|
|
915
|
+
`;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* **The role against a template the administrator picks** (ADR-0139 decision 11) — whatever the role is called.
|
|
919
|
+
*
|
|
920
|
+
* The template of the role's name is picked to begin with; a role named in the site's own words (경영지원, 인사급여)
|
|
921
|
+
* has no such pair, and the administrator picks one. The role is never renamed to make a pair: an approval line
|
|
922
|
+
* holds the role. What the role holds beyond the template is shown as wider, not wrong — the site may have widened it
|
|
923
|
+
* on purpose — and nothing takes it away. What it lacks can be added; adding is the same top-up the template button
|
|
924
|
+
* on the role list runs (`createRoleFromTemplate` onto this role's name).
|
|
925
|
+
*/
|
|
926
|
+
renderTemplate() {
|
|
927
|
+
if (this.templates === null) {
|
|
928
|
+
return html `<div review><div empty>${i18next.t('text.loading')}</div></div>`;
|
|
929
|
+
}
|
|
930
|
+
if (!this.templates.length) {
|
|
931
|
+
return html `<div review><div empty>${i18next.t('text.no template to compare with')}</div></div>`;
|
|
932
|
+
}
|
|
933
|
+
const template = this.templates.find(one => one.id === this.templateId);
|
|
934
|
+
const gap = this.gap;
|
|
935
|
+
return html `
|
|
936
|
+
<div review>
|
|
937
|
+
<select @change=${(event) => this.pickTemplate(event.target.value)}>
|
|
938
|
+
<option value="" ?selected=${!this.templateId}>${i18next.t('text.pick a template to compare with')}</option>
|
|
939
|
+
${this.templates.map(one => html `<option value=${one.id} ?selected=${one.id === this.templateId}>${one.name}</option>`)}
|
|
940
|
+
</select>
|
|
941
|
+
${this.dirty ? html `<div stale>${i18next.t('text.counted from the saved privileges')}</div>` : nothing}
|
|
942
|
+
${template?.definitionErrors.length
|
|
943
|
+
? html `<div note attention>${i18next.t('text.template cannot be applied because', { list: template.definitionErrors.join(' · ') })}</div>`
|
|
944
|
+
: nothing}
|
|
945
|
+
${gap
|
|
946
|
+
? html `
|
|
947
|
+
<h4>
|
|
948
|
+
${i18next.t('label.missing against the template')}
|
|
949
|
+
<span count>${gap.missingCount}</span>
|
|
950
|
+
${gap.missingCount && !this.inherited
|
|
951
|
+
? html `<button quiet ?disabled=${this.dirty || this.saving || Boolean(template?.definitionErrors.length)} @click=${() => this.addMissing()}>
|
|
952
|
+
${i18next.t('button.add')}
|
|
953
|
+
</button>`
|
|
954
|
+
: nothing}
|
|
955
|
+
</h4>
|
|
956
|
+
${gap.missingCount ? this.renderSentences(gap.missing) : html `<div empty>${i18next.t('text.nothing missing against the template')}</div>`}
|
|
957
|
+
<h4>
|
|
958
|
+
${i18next.t('label.wider than the template')}
|
|
959
|
+
<span count>${gap.extraCount}</span>
|
|
960
|
+
</h4>
|
|
961
|
+
${gap.extraCount ? this.renderSentences(gap.extra) : nothing}
|
|
962
|
+
`
|
|
963
|
+
: nothing}
|
|
611
964
|
</div>
|
|
612
965
|
`;
|
|
613
966
|
}
|
|
967
|
+
renderSentences(grants) {
|
|
968
|
+
return html `<ul sentences>
|
|
969
|
+
${grants.flatMap(grant => grant.axes.map(axis => html `<li>${this.sentenceOf(grant.category, axis)}</li>`))}
|
|
970
|
+
</ul>`;
|
|
971
|
+
}
|
|
972
|
+
/** A privilege in words — the sentence the privileges view draws, or its module and axis when there is none. */
|
|
973
|
+
sentenceOf(category, axis) {
|
|
974
|
+
const cell = this.modules.find(module => module.category === category)?.cells.find(one => one.axis === axis);
|
|
975
|
+
return (cell && axisText(category, cell)) || `${moduleLabel(category)} · ${axisLabel(axis)}`;
|
|
976
|
+
}
|
|
977
|
+
async fetchTemplates() {
|
|
978
|
+
const response = await client.query({
|
|
979
|
+
query: gql `
|
|
980
|
+
query {
|
|
981
|
+
roleTemplates {
|
|
982
|
+
id
|
|
983
|
+
name
|
|
984
|
+
definitionErrors
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
`,
|
|
988
|
+
context: gqlContext()
|
|
989
|
+
});
|
|
990
|
+
/* Reading templates asks the same door creating a role does; without it there is nothing to compare with. */
|
|
991
|
+
const templates = response.errors?.length ? [] : response.data?.roleTemplates || [];
|
|
992
|
+
this.templates = templates;
|
|
993
|
+
const paired = templates.find(one => one.name === this.subject?.name);
|
|
994
|
+
await this.pickTemplate(paired?.id || '');
|
|
995
|
+
}
|
|
996
|
+
async pickTemplate(templateId) {
|
|
997
|
+
this.templateId = templateId;
|
|
998
|
+
this.gap = null;
|
|
999
|
+
if (!templateId)
|
|
1000
|
+
return;
|
|
1001
|
+
const response = await client.query({
|
|
1002
|
+
query: gql `
|
|
1003
|
+
query ($filters: [Filter!], $templateId: String) {
|
|
1004
|
+
roles(filters: $filters) {
|
|
1005
|
+
items {
|
|
1006
|
+
id
|
|
1007
|
+
templateGap(templateId: $templateId) {
|
|
1008
|
+
templateId
|
|
1009
|
+
templateName
|
|
1010
|
+
missing { ${GRANT_FIELDS} }
|
|
1011
|
+
missingCount
|
|
1012
|
+
extra { ${GRANT_FIELDS} }
|
|
1013
|
+
extraCount
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
`,
|
|
1019
|
+
variables: { filters: [{ name: 'id', operator: 'eq', value: this.subject.id }], templateId },
|
|
1020
|
+
context: gqlContext()
|
|
1021
|
+
});
|
|
1022
|
+
if (response.errors?.length) {
|
|
1023
|
+
document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (this.templateId === templateId)
|
|
1027
|
+
this.gap = response.data?.roles?.items?.[0]?.templateGap || null;
|
|
1028
|
+
}
|
|
1029
|
+
/** Adds what the role lacks against the picked template — after showing what it adds. */
|
|
1030
|
+
async addMissing() {
|
|
1031
|
+
const gap = this.gap;
|
|
1032
|
+
if (!gap?.missingCount || this.dirty || this.inherited)
|
|
1033
|
+
return;
|
|
1034
|
+
const list = gap.missing.flatMap(grant => grant.axes.map(axis => this.sentenceOf(grant.category, axis))).join(', ');
|
|
1035
|
+
const confirmed = await OxPrompt.open({
|
|
1036
|
+
type: 'info',
|
|
1037
|
+
title: i18next.t('title.add template privileges'),
|
|
1038
|
+
text: i18next.t('text.add template privileges', { template: gap.templateName, count: gap.missingCount, list }),
|
|
1039
|
+
confirmButton: { text: i18next.t('button.add') },
|
|
1040
|
+
cancelButton: { text: i18next.t('button.cancel') }
|
|
1041
|
+
});
|
|
1042
|
+
if (!confirmed)
|
|
1043
|
+
return;
|
|
1044
|
+
const response = await client.mutate({
|
|
1045
|
+
mutation: gql `
|
|
1046
|
+
mutation ($templateId: String!, $name: String) {
|
|
1047
|
+
createRoleFromTemplate(templateId: $templateId, name: $name) {
|
|
1048
|
+
id
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
`,
|
|
1052
|
+
variables: { templateId: gap.templateId, name: this.subject.name },
|
|
1053
|
+
context: gqlContext()
|
|
1054
|
+
});
|
|
1055
|
+
if (response.errors?.length) {
|
|
1056
|
+
document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
document.dispatchEvent(new CustomEvent('notify', { detail: { message: i18next.t('text.data_updated_successfully') } }));
|
|
1060
|
+
await this.fetch();
|
|
1061
|
+
await this.pickTemplate(gap.templateId);
|
|
1062
|
+
}
|
|
614
1063
|
/**
|
|
615
1064
|
* 한 줄 — 문장과 [떼기].
|
|
616
1065
|
*
|
|
@@ -861,6 +1310,17 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
861
1310
|
name
|
|
862
1311
|
email
|
|
863
1312
|
}
|
|
1313
|
+
taskCoverage {
|
|
1314
|
+
tasks {
|
|
1315
|
+
taskId
|
|
1316
|
+
module
|
|
1317
|
+
status
|
|
1318
|
+
held { ${GRANT_FIELDS} }
|
|
1319
|
+
missing { ${GRANT_FIELDS} }
|
|
1320
|
+
undeclared { ${GRANT_FIELDS} }
|
|
1321
|
+
}
|
|
1322
|
+
unclassified { ${GRANT_FIELDS} }
|
|
1323
|
+
}
|
|
864
1324
|
}
|
|
865
1325
|
}
|
|
866
1326
|
}
|
|
@@ -887,6 +1347,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
|
|
|
887
1347
|
}
|
|
888
1348
|
this.subject = role;
|
|
889
1349
|
this.holders = this.subject.users || [];
|
|
1350
|
+
this.coverage = role.taskCoverage || { tasks: [], unclassified: [] };
|
|
890
1351
|
this.build(response.data?.rolePrivileges || []);
|
|
891
1352
|
this.fetchState = 'loaded';
|
|
892
1353
|
/*
|
|
@@ -1047,6 +1508,30 @@ __decorate([
|
|
|
1047
1508
|
state(),
|
|
1048
1509
|
__metadata("design:type", Boolean)
|
|
1049
1510
|
], RolePrivilegePage.prototype, "dirty", void 0);
|
|
1511
|
+
__decorate([
|
|
1512
|
+
state(),
|
|
1513
|
+
__metadata("design:type", String)
|
|
1514
|
+
], RolePrivilegePage.prototype, "view", void 0);
|
|
1515
|
+
__decorate([
|
|
1516
|
+
state(),
|
|
1517
|
+
__metadata("design:type", Object)
|
|
1518
|
+
], RolePrivilegePage.prototype, "coverage", void 0);
|
|
1519
|
+
__decorate([
|
|
1520
|
+
state(),
|
|
1521
|
+
__metadata("design:type", Boolean)
|
|
1522
|
+
], RolePrivilegePage.prototype, "showUnheldTasks", void 0);
|
|
1523
|
+
__decorate([
|
|
1524
|
+
state(),
|
|
1525
|
+
__metadata("design:type", Object)
|
|
1526
|
+
], RolePrivilegePage.prototype, "templates", void 0);
|
|
1527
|
+
__decorate([
|
|
1528
|
+
state(),
|
|
1529
|
+
__metadata("design:type", String)
|
|
1530
|
+
], RolePrivilegePage.prototype, "templateId", void 0);
|
|
1531
|
+
__decorate([
|
|
1532
|
+
state(),
|
|
1533
|
+
__metadata("design:type", Object)
|
|
1534
|
+
], RolePrivilegePage.prototype, "gap", void 0);
|
|
1050
1535
|
RolePrivilegePage = __decorate([
|
|
1051
1536
|
customElement('role-privilege-page')
|
|
1052
1537
|
], RolePrivilegePage);
|
|
@@ -1096,4 +1581,14 @@ function axisOrder(axis) {
|
|
|
1096
1581
|
const index = KNOWN_AXES.indexOf(axis);
|
|
1097
1582
|
return index < 0 ? KNOWN_AXES.length : index;
|
|
1098
1583
|
}
|
|
1584
|
+
/** A task's name in the words of the module that declares it (`task.<id>.name`); the id when there are none. */
|
|
1585
|
+
function taskName(id) {
|
|
1586
|
+
return i18next.t(`task.${id}.name`, { defaultValue: id });
|
|
1587
|
+
}
|
|
1588
|
+
function statusOrder(status) {
|
|
1589
|
+
return ['broken', 'all', 'some', 'none'].indexOf(status);
|
|
1590
|
+
}
|
|
1591
|
+
function countOf(grants) {
|
|
1592
|
+
return grants.reduce((count, grant) => count + grant.axes.length, 0);
|
|
1593
|
+
}
|
|
1099
1594
|
//# sourceMappingURL=role-privilege-page.js.map
|