@things-factory/auth-ui 10.1.194 → 10.1.198

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 (44) hide show
  1. package/dist-client/bootstrap.js +4 -1
  2. package/dist-client/bootstrap.js.map +1 -1
  3. package/dist-client/components/my-login-history.js +14 -10
  4. package/dist-client/components/my-login-history.js.map +1 -1
  5. package/dist-client/components/role-template-picker.d.ts +5 -0
  6. package/dist-client/components/role-template-picker.js +41 -5
  7. package/dist-client/components/role-template-picker.js.map +1 -1
  8. package/dist-client/components/user-status-renderer.d.ts +9 -0
  9. package/dist-client/components/user-status-renderer.js +36 -0
  10. package/dist-client/components/user-status-renderer.js.map +1 -0
  11. package/dist-client/entity360/person-360.d.ts +13 -0
  12. package/dist-client/entity360/person-360.js +96 -0
  13. package/dist-client/entity360/person-360.js.map +1 -1
  14. package/dist-client/index.d.ts +1 -0
  15. package/dist-client/index.js +14 -1
  16. package/dist-client/index.js.map +1 -1
  17. package/dist-client/pages/audit/audit-page.d.ts +46 -0
  18. package/dist-client/pages/audit/audit-page.js +417 -0
  19. package/dist-client/pages/audit/audit-page.js.map +1 -0
  20. package/dist-client/pages/role/role-privilege-page.d.ts +38 -0
  21. package/dist-client/pages/role/role-privilege-page.js +441 -5
  22. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  23. package/dist-client/pages/role/role-task-review.test.d.ts +1 -0
  24. package/dist-client/pages/role/role-task-review.test.js +49 -0
  25. package/dist-client/pages/role/role-task-review.test.js.map +1 -0
  26. package/dist-client/pages/user/user-list-page.js +1 -31
  27. package/dist-client/pages/user/user-list-page.js.map +1 -1
  28. package/dist-client/route.js +3 -0
  29. package/dist-client/route.js.map +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/dist-client/utils/audit-words.d.ts +30 -0
  32. package/dist-client/utils/audit-words.js +57 -0
  33. package/dist-client/utils/audit-words.js.map +1 -0
  34. package/dist-client/utils/audit-words.test.d.ts +1 -0
  35. package/dist-client/utils/audit-words.test.js +47 -0
  36. package/dist-client/utils/audit-words.test.js.map +1 -0
  37. package/package.json +4 -4
  38. package/things-factory.config.js +1 -0
  39. package/translations/en.json +68 -1
  40. package/translations/es.json +68 -1
  41. package/translations/ja.json +68 -1
  42. package/translations/ko.json +68 -1
  43. package/translations/ms.json +68 -1
  44. package/translations/zh.json +68 -1
@@ -9,10 +9,12 @@ 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
+ const GRANT_FIELDS = 'category axes';
16
18
  /**
17
19
  * 한 역할이 무엇을 할 수 있나 — **켜진 것이 문장으로 본문에 선다.**
18
20
  *
@@ -61,6 +63,14 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
61
63
  this.loadError = null;
62
64
  this.saving = false;
63
65
  this.dirty = false;
66
+ this.view = 'privileges';
67
+ /** The role laid against every task - computed by the server from the saved privileges (`Role.taskCoverage`). */
68
+ this.coverage = { tasks: [], unclassified: [] };
69
+ this.showUnheldTasks = false;
70
+ /** null until asked for; [] when this person may not read templates (`roleTemplates` asks user:mutation). */
71
+ this.templates = null;
72
+ this.templateId = '';
73
+ this.gap = null;
64
74
  }
65
75
  static { this.styles = [
66
76
  /* The frame owns the page shell; the two panes are the content it sizes (ADR-0060 decision 8). */
@@ -447,6 +457,157 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
447
457
  color: var(--md-sys-color-on-surface-variant);
448
458
  font-size: 0.85rem;
449
459
  }
460
+
461
+ /*
462
+ The three readings of one role - quiet text buttons in the panel's header line, the chosen one underlined.
463
+ They change what the panel shows, not what the role holds, so they are not boxed like an action.
464
+ */
465
+ [granted] > header nav {
466
+ margin-left: auto;
467
+ display: flex;
468
+ gap: 2px;
469
+ }
470
+
471
+ [granted] > header nav button {
472
+ padding: 2px 8px;
473
+ border: none;
474
+ border-bottom: 2px solid transparent;
475
+ background: none;
476
+ font: inherit;
477
+ font-size: 0.8rem;
478
+ color: var(--md-sys-color-on-surface-variant);
479
+ cursor: pointer;
480
+ }
481
+
482
+ [granted] > header nav button[selected] {
483
+ border-bottom-color: var(--md-sys-color-primary);
484
+ color: var(--md-sys-color-on-surface);
485
+ font-weight: 600;
486
+ }
487
+
488
+ [review] {
489
+ flex: 1;
490
+ min-height: 0;
491
+ overflow: auto;
492
+ padding: 0 12px 12px;
493
+ }
494
+
495
+ [review] h4 {
496
+ display: flex;
497
+ align-items: baseline;
498
+ gap: 8px;
499
+ margin: 14px 0 4px;
500
+ font-size: 0.85rem;
501
+ font-weight: 600;
502
+ color: var(--md-sys-color-on-surface-variant);
503
+ }
504
+
505
+ [review] h4 [count] {
506
+ font-weight: 400;
507
+ color: var(--md-sys-color-outline);
508
+ }
509
+
510
+ ul[tasks] {
511
+ list-style: none;
512
+ margin: 0;
513
+ padding: 0;
514
+ }
515
+
516
+ ul[tasks] > li {
517
+ padding: 9px 2px;
518
+ border-bottom: 1px solid var(--md-sys-color-surface-variant);
519
+ }
520
+
521
+ ul[tasks] > li:last-child {
522
+ border-bottom: none;
523
+ }
524
+
525
+ ul[tasks] [task-name] {
526
+ font-size: 0.95rem;
527
+ font-weight: 600;
528
+ }
529
+
530
+ ul[tasks] [task-says],
531
+ ul[tasks] [task-missing] {
532
+ margin-top: 2px;
533
+ font-size: 0.85rem;
534
+ line-height: 1.55;
535
+ color: var(--md-sys-color-on-surface-variant);
536
+ }
537
+
538
+ ul[tasks] [task-missing] {
539
+ color: var(--md-sys-color-outline);
540
+ }
541
+
542
+ ul[tasks][quiet] > li {
543
+ padding: 5px 2px;
544
+ font-size: 0.88rem;
545
+ color: var(--md-sys-color-on-surface-variant);
546
+ }
547
+
548
+ [state] {
549
+ margin-left: 8px;
550
+ padding: 0 6px;
551
+ border-radius: var(--md-sys-shape-corner-full, 999px);
552
+ font-size: 0.75rem;
553
+ font-weight: 400;
554
+ background-color: var(--md-sys-color-surface-container-highest);
555
+ color: var(--md-sys-color-on-surface-variant);
556
+ }
557
+
558
+ [state][all] {
559
+ background-color: var(--md-sys-color-secondary-container);
560
+ color: var(--md-sys-color-on-secondary-container);
561
+ }
562
+
563
+ [state][broken] {
564
+ background-color: var(--md-sys-color-error-container);
565
+ color: var(--md-sys-color-on-error-container);
566
+ }
567
+
568
+ ul[sentences] {
569
+ list-style: none;
570
+ margin: 0;
571
+ padding: 0;
572
+ }
573
+
574
+ ul[sentences] li {
575
+ padding: 5px 2px;
576
+ font-size: 0.9rem;
577
+ line-height: 1.5;
578
+ }
579
+
580
+ button[quiet] {
581
+ padding: 4px 0;
582
+ border: none;
583
+ background: none;
584
+ font: inherit;
585
+ font-size: 0.85rem;
586
+ color: var(--md-sys-color-primary);
587
+ cursor: pointer;
588
+ }
589
+
590
+ button[quiet]:disabled {
591
+ color: var(--md-sys-color-outline);
592
+ cursor: default;
593
+ }
594
+
595
+ [review] select {
596
+ margin-top: 10px;
597
+ padding: 4px 6px;
598
+ font: inherit;
599
+ font-size: 0.88rem;
600
+ color: var(--md-sys-color-on-surface);
601
+ background-color: var(--md-sys-color-surface);
602
+ border: 1px solid var(--md-sys-color-outline-variant);
603
+ border-radius: var(--md-sys-shape-corner-small, 6px);
604
+ }
605
+
606
+ [review] [stale] {
607
+ margin-top: 10px;
608
+ font-size: 0.82rem;
609
+ color: var(--md-sys-color-outline);
610
+ }
450
611
  `
451
612
  ]; }
452
613
  /**
@@ -600,17 +761,246 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
600
761
  <header>
601
762
  ${i18next.t('field.granted privileges')}
602
763
  <span count>${granted.length}</span>
764
+ <nav>
765
+ ${this.viewButton('privileges', i18next.t('label.privileges view'))} ${this.viewButton('tasks', i18next.t('label.tasks view'))}
766
+ ${this.viewButton('template', i18next.t('label.template view'))}
767
+ </nav>
603
768
  </header>
604
769
 
605
- ${granted.length
606
- ? html `<ul lines>
607
- ${granted.map(({ category, cell }) => this.renderGrantedLine(category, cell))}
770
+ ${this.view === 'tasks'
771
+ ? this.renderTasks()
772
+ : this.view === 'template'
773
+ ? this.renderTemplate()
774
+ : html `${granted.length
775
+ ? html `<ul lines>
776
+ ${granted.map(({ category, cell }) => this.renderGrantedLine(category, cell))}
777
+ </ul>`
778
+ : html `<div empty>${i18next.t('text.nothing is on for this role yet')}</div>`}
779
+ ${this.renderAdd()}`}
780
+ </div>
781
+ `;
782
+ }
783
+ viewButton(view, label) {
784
+ return html `<button ?selected=${this.view === view} @click=${() => this.showView(view)}>${label}</button>`;
785
+ }
786
+ async showView(view) {
787
+ this.view = view;
788
+ if (view === 'template' && this.templates === null) {
789
+ await this.fetchTemplates();
790
+ }
791
+ }
792
+ /**
793
+ * **The role by task** (ADR-0139 decision 11) — which of each task's privileges this role holds, as the server
794
+ * computes it from the saved privileges.
795
+ *
796
+ * It says what the role holds, nothing more: 「전부」 is not "this person can do the work" (another role, the
797
+ * data's own scope or an approval line decide that too), and 「일부」 is not a fault — a privilege several tasks
798
+ * share makes each of them 「일부」. So nothing here offers to add or take away; the privileges view and the
799
+ * template view do that.
800
+ */
801
+ renderTasks() {
802
+ const tasks = this.coverage.tasks;
803
+ const held = tasks.filter(task => task.status !== 'none').sort((a, b) => statusOrder(a.status) - statusOrder(b.status));
804
+ const unheld = tasks.filter(task => task.status === 'none');
805
+ return html `
806
+ <div review>
807
+ ${this.dirty ? html `<div stale>${i18next.t('text.counted from the saved privileges')}</div>` : nothing}
808
+ ${held.length
809
+ ? html `<ul tasks>
810
+ ${held.map(task => this.renderTask(task))}
608
811
  </ul>`
609
- : html `<div empty>${i18next.t('text.nothing is on for this role yet')}</div>`}
610
- ${this.renderAdd()}
812
+ : html `<div empty>${i18next.t('text.this role holds no task')}</div>`}
813
+ ${unheld.length
814
+ ? html `<h4>
815
+ ${i18next.t('label.tasks this role does not hold')}
816
+ <span count>${unheld.length}</span>
817
+ <button quiet @click=${() => (this.showUnheldTasks = !this.showUnheldTasks)}>
818
+ ${this.showUnheldTasks ? i18next.t('button.fold') : i18next.t('button.unfold')}
819
+ </button>
820
+ </h4>
821
+ ${this.showUnheldTasks
822
+ ? html `<ul tasks quiet>
823
+ ${unheld.map(task => html `<li>${taskName(task.taskId)}</li>`)}
824
+ </ul>`
825
+ : nothing}`
826
+ : nothing}
827
+ ${this.coverage.unclassified.length
828
+ ? html `<h4>
829
+ ${i18next.t('label.privileges no task names')}
830
+ <span count>${countOf(this.coverage.unclassified)}</span>
831
+ </h4>
832
+ ${this.renderSentences(this.coverage.unclassified)}`
833
+ : nothing}
611
834
  </div>
612
835
  `;
613
836
  }
837
+ renderTask(task) {
838
+ const held = countOf(task.held);
839
+ const total = held + countOf(task.missing);
840
+ const says = i18next.t(`task.${task.taskId}.description`, { defaultValue: '' });
841
+ return html `
842
+ <li>
843
+ <div>
844
+ <span task-name>${taskName(task.taskId)}</span>
845
+ ${task.status === 'broken'
846
+ ? html `<span state broken>${i18next.t('text.task names an undeclared privilege')}</span>`
847
+ : task.status === 'all'
848
+ ? html `<span state all>${i18next.t('text.task held whole')}</span>`
849
+ : html `<span state>${i18next.t('text.task held in part', { held, total })}</span>`}
850
+ </div>
851
+ ${says ? html `<div task-says>${says}</div>` : nothing}
852
+ ${task.status === 'some'
853
+ ? 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>`
854
+ : nothing}
855
+ </li>
856
+ `;
857
+ }
858
+ /**
859
+ * **The role against a template the administrator picks** (ADR-0139 decision 11) — whatever the role is called.
860
+ *
861
+ * The template of the role's name is picked to begin with; a role named in the site's own words (경영지원, 인사급여)
862
+ * has no such pair, and the administrator picks one. The role is never renamed to make a pair: an approval line
863
+ * holds the role. What the role holds beyond the template is shown as wider, not wrong — the site may have widened it
864
+ * on purpose — and nothing takes it away. What it lacks can be added; adding is the same top-up the template button
865
+ * on the role list runs (`createRoleFromTemplate` onto this role's name).
866
+ */
867
+ renderTemplate() {
868
+ if (this.templates === null) {
869
+ return html `<div review><div empty>${i18next.t('text.loading')}</div></div>`;
870
+ }
871
+ if (!this.templates.length) {
872
+ return html `<div review><div empty>${i18next.t('text.no template to compare with')}</div></div>`;
873
+ }
874
+ const template = this.templates.find(one => one.id === this.templateId);
875
+ const gap = this.gap;
876
+ return html `
877
+ <div review>
878
+ <select @change=${(event) => this.pickTemplate(event.target.value)}>
879
+ <option value="" ?selected=${!this.templateId}>${i18next.t('text.pick a template to compare with')}</option>
880
+ ${this.templates.map(one => html `<option value=${one.id} ?selected=${one.id === this.templateId}>${one.name}</option>`)}
881
+ </select>
882
+ ${this.dirty ? html `<div stale>${i18next.t('text.counted from the saved privileges')}</div>` : nothing}
883
+ ${template?.definitionErrors.length
884
+ ? html `<div note attention>${i18next.t('text.template cannot be applied because', { list: template.definitionErrors.join(' · ') })}</div>`
885
+ : nothing}
886
+ ${gap
887
+ ? html `
888
+ <h4>
889
+ ${i18next.t('label.missing against the template')}
890
+ <span count>${gap.missingCount}</span>
891
+ ${gap.missingCount && !this.inherited
892
+ ? html `<button quiet ?disabled=${this.dirty || this.saving || Boolean(template?.definitionErrors.length)} @click=${() => this.addMissing()}>
893
+ ${i18next.t('button.add')}
894
+ </button>`
895
+ : nothing}
896
+ </h4>
897
+ ${gap.missingCount ? this.renderSentences(gap.missing) : html `<div empty>${i18next.t('text.nothing missing against the template')}</div>`}
898
+ <h4>
899
+ ${i18next.t('label.wider than the template')}
900
+ <span count>${gap.extraCount}</span>
901
+ </h4>
902
+ ${gap.extraCount ? this.renderSentences(gap.extra) : nothing}
903
+ `
904
+ : nothing}
905
+ </div>
906
+ `;
907
+ }
908
+ renderSentences(grants) {
909
+ return html `<ul sentences>
910
+ ${grants.flatMap(grant => grant.axes.map(axis => html `<li>${this.sentenceOf(grant.category, axis)}</li>`))}
911
+ </ul>`;
912
+ }
913
+ /** A privilege in words — the sentence the privileges view draws, or its module and axis when there is none. */
914
+ sentenceOf(category, axis) {
915
+ const cell = this.modules.find(module => module.category === category)?.cells.find(one => one.axis === axis);
916
+ return (cell && axisText(category, cell)) || `${moduleLabel(category)} · ${axisLabel(axis)}`;
917
+ }
918
+ async fetchTemplates() {
919
+ const response = await client.query({
920
+ query: gql `
921
+ query {
922
+ roleTemplates {
923
+ id
924
+ name
925
+ definitionErrors
926
+ }
927
+ }
928
+ `,
929
+ context: gqlContext()
930
+ });
931
+ /* Reading templates asks the same door creating a role does; without it there is nothing to compare with. */
932
+ const templates = response.errors?.length ? [] : response.data?.roleTemplates || [];
933
+ this.templates = templates;
934
+ const paired = templates.find(one => one.name === this.subject?.name);
935
+ await this.pickTemplate(paired?.id || '');
936
+ }
937
+ async pickTemplate(templateId) {
938
+ this.templateId = templateId;
939
+ this.gap = null;
940
+ if (!templateId)
941
+ return;
942
+ const response = await client.query({
943
+ query: gql `
944
+ query ($filters: [Filter!], $templateId: String) {
945
+ roles(filters: $filters) {
946
+ items {
947
+ id
948
+ templateGap(templateId: $templateId) {
949
+ templateId
950
+ templateName
951
+ missing { ${GRANT_FIELDS} }
952
+ missingCount
953
+ extra { ${GRANT_FIELDS} }
954
+ extraCount
955
+ }
956
+ }
957
+ }
958
+ }
959
+ `,
960
+ variables: { filters: [{ name: 'id', operator: 'eq', value: this.subject.id }], templateId },
961
+ context: gqlContext()
962
+ });
963
+ if (response.errors?.length) {
964
+ document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
965
+ return;
966
+ }
967
+ if (this.templateId === templateId)
968
+ this.gap = response.data?.roles?.items?.[0]?.templateGap || null;
969
+ }
970
+ /** Adds what the role lacks against the picked template — after showing what it adds. */
971
+ async addMissing() {
972
+ const gap = this.gap;
973
+ if (!gap?.missingCount || this.dirty || this.inherited)
974
+ return;
975
+ const list = gap.missing.flatMap(grant => grant.axes.map(axis => this.sentenceOf(grant.category, axis))).join(', ');
976
+ const confirmed = await OxPrompt.open({
977
+ type: 'info',
978
+ title: i18next.t('title.add template privileges'),
979
+ text: i18next.t('text.add template privileges', { template: gap.templateName, count: gap.missingCount, list }),
980
+ confirmButton: { text: i18next.t('button.add') },
981
+ cancelButton: { text: i18next.t('button.cancel') }
982
+ });
983
+ if (!confirmed)
984
+ return;
985
+ const response = await client.mutate({
986
+ mutation: gql `
987
+ mutation ($templateId: String!, $name: String) {
988
+ createRoleFromTemplate(templateId: $templateId, name: $name) {
989
+ id
990
+ }
991
+ }
992
+ `,
993
+ variables: { templateId: gap.templateId, name: this.subject.name },
994
+ context: gqlContext()
995
+ });
996
+ if (response.errors?.length) {
997
+ document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
998
+ return;
999
+ }
1000
+ document.dispatchEvent(new CustomEvent('notify', { detail: { message: i18next.t('text.data_updated_successfully') } }));
1001
+ await this.fetch();
1002
+ await this.pickTemplate(gap.templateId);
1003
+ }
614
1004
  /**
615
1005
  * 한 줄 — 문장과 [떼기].
616
1006
  *
@@ -861,6 +1251,17 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
861
1251
  name
862
1252
  email
863
1253
  }
1254
+ taskCoverage {
1255
+ tasks {
1256
+ taskId
1257
+ module
1258
+ status
1259
+ held { ${GRANT_FIELDS} }
1260
+ missing { ${GRANT_FIELDS} }
1261
+ undeclared { ${GRANT_FIELDS} }
1262
+ }
1263
+ unclassified { ${GRANT_FIELDS} }
1264
+ }
864
1265
  }
865
1266
  }
866
1267
  }
@@ -887,6 +1288,7 @@ let RolePrivilegePage = class RolePrivilegePage extends localize(i18next)(PageVi
887
1288
  }
888
1289
  this.subject = role;
889
1290
  this.holders = this.subject.users || [];
1291
+ this.coverage = role.taskCoverage || { tasks: [], unclassified: [] };
890
1292
  this.build(response.data?.rolePrivileges || []);
891
1293
  this.fetchState = 'loaded';
892
1294
  /*
@@ -1047,6 +1449,30 @@ __decorate([
1047
1449
  state(),
1048
1450
  __metadata("design:type", Boolean)
1049
1451
  ], RolePrivilegePage.prototype, "dirty", void 0);
1452
+ __decorate([
1453
+ state(),
1454
+ __metadata("design:type", String)
1455
+ ], RolePrivilegePage.prototype, "view", void 0);
1456
+ __decorate([
1457
+ state(),
1458
+ __metadata("design:type", Object)
1459
+ ], RolePrivilegePage.prototype, "coverage", void 0);
1460
+ __decorate([
1461
+ state(),
1462
+ __metadata("design:type", Boolean)
1463
+ ], RolePrivilegePage.prototype, "showUnheldTasks", void 0);
1464
+ __decorate([
1465
+ state(),
1466
+ __metadata("design:type", Object)
1467
+ ], RolePrivilegePage.prototype, "templates", void 0);
1468
+ __decorate([
1469
+ state(),
1470
+ __metadata("design:type", String)
1471
+ ], RolePrivilegePage.prototype, "templateId", void 0);
1472
+ __decorate([
1473
+ state(),
1474
+ __metadata("design:type", Object)
1475
+ ], RolePrivilegePage.prototype, "gap", void 0);
1050
1476
  RolePrivilegePage = __decorate([
1051
1477
  customElement('role-privilege-page')
1052
1478
  ], RolePrivilegePage);
@@ -1096,4 +1522,14 @@ function axisOrder(axis) {
1096
1522
  const index = KNOWN_AXES.indexOf(axis);
1097
1523
  return index < 0 ? KNOWN_AXES.length : index;
1098
1524
  }
1525
+ /** A task's name in the words of the module that declares it (`task.<id>.name`); the id when there are none. */
1526
+ function taskName(id) {
1527
+ return i18next.t(`task.${id}.name`, { defaultValue: id });
1528
+ }
1529
+ function statusOrder(status) {
1530
+ return ['broken', 'all', 'some', 'none'].indexOf(status);
1531
+ }
1532
+ function countOf(grants) {
1533
+ return grants.reduce((count, grant) => count + grant.axes.length, 0);
1534
+ }
1099
1535
  //# sourceMappingURL=role-privilege-page.js.map