@things-factory/auth-ui 10.1.241 → 10.1.245
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/components/my-role-tasks.d.ts +27 -0
- package/dist-client/components/my-role-tasks.js +30 -0
- package/dist-client/components/my-role-tasks.js.map +1 -0
- package/dist-client/components/my-role-tasks.test.d.ts +1 -0
- package/dist-client/components/my-role-tasks.test.js +69 -0
- package/dist-client/components/my-role-tasks.test.js.map +1 -0
- package/dist-client/components/my-roles.d.ts +18 -0
- package/dist-client/components/my-roles.js +211 -0
- package/dist-client/components/my-roles.js.map +1 -0
- package/dist-client/pages/audit/audit-page.d.ts +4 -0
- package/dist-client/pages/audit/audit-page.js +52 -13
- package/dist-client/pages/audit/audit-page.js.map +1 -1
- package/dist-client/pages/my-roles-page.d.ts +21 -0
- package/dist-client/pages/my-roles-page.js +39 -0
- package/dist-client/pages/my-roles-page.js.map +1 -0
- package/dist-client/pages/profile.d.ts +0 -5
- package/dist-client/pages/profile.js +2 -3
- package/dist-client/pages/profile.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-navigation.test.js +1 -1
- package/dist-client/utils/access-request-navigation.test.js.map +1 -1
- package/package.json +6 -6
- package/things-factory.config.js +2 -1
- package/translations/en.json +7 -1
- package/translations/es.json +7 -1
- package/translations/ja.json +7 -1
- package/translations/ko.json +7 -1
- package/translations/ms.json +7 -1
- package/translations/zh.json +7 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TaskCoverageRef, TaskGrant } from './role-task-edit.js';
|
|
2
|
+
export type MyRoleRef = {
|
|
3
|
+
name: string;
|
|
4
|
+
taskCoverage: {
|
|
5
|
+
tasks: TaskCoverageRef[];
|
|
6
|
+
unclassified: TaskGrant[];
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export type MyWork = {
|
|
10
|
+
taskId: string;
|
|
11
|
+
roles: string[];
|
|
12
|
+
};
|
|
13
|
+
export type MyPrivilege = {
|
|
14
|
+
category: string;
|
|
15
|
+
axis: string;
|
|
16
|
+
roles: string[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The work across one's roles: each task held whole by any of them, once, with the roles that give it — leaving out a
|
|
20
|
+
* task whose privileges another shown task already holds and more (reading the sales under keeping them says the same
|
|
21
|
+
* thing twice). `shared` is what every role gives, when there are two roles or more; `own` the rest, in role order.
|
|
22
|
+
*/
|
|
23
|
+
export declare function workOfRoles(roles: MyRoleRef[]): {
|
|
24
|
+
own: MyWork[];
|
|
25
|
+
shared: MyWork[];
|
|
26
|
+
alone: MyPrivilege[];
|
|
27
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const pairsOf = (grants) => new Set(grants.flatMap(grant => grant.axes.map(axis => `${grant.category}:${axis}`)));
|
|
2
|
+
const within = (inner, outer) => inner.size < outer.size && [...inner].every(pair => outer.has(pair));
|
|
3
|
+
/**
|
|
4
|
+
* The work across one's roles: each task held whole by any of them, once, with the roles that give it — leaving out a
|
|
5
|
+
* task whose privileges another shown task already holds and more (reading the sales under keeping them says the same
|
|
6
|
+
* thing twice). `shared` is what every role gives, when there are two roles or more; `own` the rest, in role order.
|
|
7
|
+
*/
|
|
8
|
+
export function workOfRoles(roles) {
|
|
9
|
+
const work = new Map();
|
|
10
|
+
for (const role of roles)
|
|
11
|
+
for (const task of role.taskCoverage.tasks.filter(task => task.status === 'all')) {
|
|
12
|
+
const one = work.get(task.taskId) || { taskId: task.taskId, roles: [], held: pairsOf(task.held) };
|
|
13
|
+
one.roles.push(role.name);
|
|
14
|
+
work.set(task.taskId, one);
|
|
15
|
+
}
|
|
16
|
+
const all = [...work.values()];
|
|
17
|
+
const shown = all.filter(task => !all.some(other => other !== task && within(task.held, other.held))).map(({ taskId, roles }) => ({ taskId, roles }));
|
|
18
|
+
const everyRole = (task) => roles.length > 1 && task.roles.length === roles.length;
|
|
19
|
+
const alone = new Map();
|
|
20
|
+
for (const role of roles)
|
|
21
|
+
for (const grant of role.taskCoverage.unclassified)
|
|
22
|
+
for (const axis of grant.axes) {
|
|
23
|
+
const key = `${grant.category}:${axis}`;
|
|
24
|
+
const one = alone.get(key) || { category: grant.category, axis, roles: [] };
|
|
25
|
+
one.roles.push(role.name);
|
|
26
|
+
alone.set(key, one);
|
|
27
|
+
}
|
|
28
|
+
return { own: shown.filter(task => !everyRole(task)), shared: shown.filter(everyRole), alone: [...alone.values()] };
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=my-role-tasks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"my-role-tasks.js","sourceRoot":"","sources":["../../client/components/my-role-tasks.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What one's own roles are said to hold (ADR-0139 §11-11) — the rule, apart from the popup so it is tested without a DOM.\n *\n * Every list comes from the server's `RoleBrief.taskCoverage`, the same reading the role screen groups by. A person is\n * told the work a role holds whole; a task held in part is not work they can name (a privilege shared by several tasks\n * makes each of them 「일부」), and its privileges are said by the whole task that holds them or as a privilege alone.\n *\n * A person asks what they can do, and why: the work is one list across their roles, each task once with the roles that\n * give it, and what every role of theirs gives is said once, apart (「이해하기 쉽게」, the user 2026-11-01).\n */\nimport type { TaskCoverageRef, TaskGrant } from './role-task-edit.js'\n\nexport type MyRoleRef = { name: string; taskCoverage: { tasks: TaskCoverageRef[]; unclassified: TaskGrant[] } }\nexport type MyWork = { taskId: string; roles: string[] }\nexport type MyPrivilege = { category: string; axis: string; roles: string[] }\n\nconst pairsOf = (grants: TaskGrant[]) => new Set(grants.flatMap(grant => grant.axes.map(axis => `${grant.category}:${axis}`)))\nconst within = (inner: Set<string>, outer: Set<string>) => inner.size < outer.size && [...inner].every(pair => outer.has(pair))\n\n/**\n * The work across one's roles: each task held whole by any of them, once, with the roles that give it — leaving out a\n * task whose privileges another shown task already holds and more (reading the sales under keeping them says the same\n * thing twice). `shared` is what every role gives, when there are two roles or more; `own` the rest, in role order.\n */\nexport function workOfRoles(roles: MyRoleRef[]): { own: MyWork[]; shared: MyWork[]; alone: MyPrivilege[] } {\n const work = new Map<string, MyWork & { held: Set<string> }>()\n for (const role of roles)\n for (const task of role.taskCoverage.tasks.filter(task => task.status === 'all')) {\n const one = work.get(task.taskId) || { taskId: task.taskId, roles: [], held: pairsOf(task.held) }\n one.roles.push(role.name)\n work.set(task.taskId, one)\n }\n\n const all = [...work.values()]\n const shown = all.filter(task => !all.some(other => other !== task && within(task.held, other.held))).map(({ taskId, roles }) => ({ taskId, roles }))\n const everyRole = (task: MyWork) => roles.length > 1 && task.roles.length === roles.length\n\n const alone = new Map<string, MyPrivilege>()\n for (const role of roles)\n for (const grant of role.taskCoverage.unclassified)\n for (const axis of grant.axes) {\n const key = `${grant.category}:${axis}`\n const one = alone.get(key) || { category: grant.category, axis, roles: [] }\n one.roles.push(role.name)\n alone.set(key, one)\n }\n\n return { own: shown.filter(task => !everyRole(task)), shared: shown.filter(everyRole), alone: [...alone.values()] }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { workOfRoles } from './my-role-tasks';
|
|
2
|
+
const task = (taskId, status, held, missing = []) => {
|
|
3
|
+
const grants = (pairs) => pairs.map(pair => ({ category: pair.split(':')[0], axes: [pair.split(':')[1]] }));
|
|
4
|
+
return { taskId, status, held: grants(held), missing: grants(missing) };
|
|
5
|
+
};
|
|
6
|
+
const role = (name, tasks, unclassified = []) => ({
|
|
7
|
+
name,
|
|
8
|
+
taskCoverage: { tasks, unclassified: unclassified.map(pair => ({ category: pair.split(':')[0], axes: [pair.split(':')[1]] })) }
|
|
9
|
+
});
|
|
10
|
+
const ids = (work) => work.map(one => one.taskId);
|
|
11
|
+
const self = () => task('office.self', 'all', ['office-self:query', 'office-self:mutation']);
|
|
12
|
+
describe('the work my roles are said to hold', () => {
|
|
13
|
+
it('★ is the tasks a role holds whole - not one held in part, nor one naming an undeclared privilege', () => {
|
|
14
|
+
const { own } = workOfRoles([
|
|
15
|
+
role('직원', [
|
|
16
|
+
self(),
|
|
17
|
+
task('office.payroll', 'none', [], ['office-payroll:query']),
|
|
18
|
+
task('accounting.expense-authoring', 'some', ['counterparty-published:query'], ['accounting-self:mutation']),
|
|
19
|
+
task('board.exporting', 'broken', [], ['board:query'])
|
|
20
|
+
])
|
|
21
|
+
]);
|
|
22
|
+
expect(ids(own)).toEqual(['office.self']);
|
|
23
|
+
});
|
|
24
|
+
it('★ says each task once across my roles, with the roles that give it', () => {
|
|
25
|
+
const { own } = workOfRoles([
|
|
26
|
+
role('경영지원', [self(), task('office.checks', 'all', ['office-check:query'])]),
|
|
27
|
+
role('인사급여', [self(), task('office.checks', 'all', ['office-check:query']), task('office.payroll', 'all', ['office-payroll:query'])]),
|
|
28
|
+
role('직원', [self()])
|
|
29
|
+
]);
|
|
30
|
+
expect(own).toEqual([
|
|
31
|
+
{ taskId: 'office.checks', roles: ['경영지원', '인사급여'] },
|
|
32
|
+
{ taskId: 'office.payroll', roles: ['인사급여'] }
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
it('★ says what every role of mine gives once, apart - and with one role, nothing is apart', () => {
|
|
36
|
+
const two = workOfRoles([role('직원', [self()]), role('팀장', [self(), task('office.team', 'all', ['office-team:query'])])]);
|
|
37
|
+
expect(ids(two.shared)).toEqual(['office.self']);
|
|
38
|
+
expect(ids(two.own)).toEqual(['office.team']);
|
|
39
|
+
const one = workOfRoles([role('직원', [self()])]);
|
|
40
|
+
expect(ids(one.shared)).toEqual([]);
|
|
41
|
+
expect(ids(one.own)).toEqual(['office.self']);
|
|
42
|
+
});
|
|
43
|
+
it('★ leaves out a task whose privileges a wider one I hold already has, whichever role gives it', () => {
|
|
44
|
+
const { own } = workOfRoles([
|
|
45
|
+
role('경영진', [task('sales.reading', 'all', ['sales:query'])]),
|
|
46
|
+
role('대표', [task('sales.keeping', 'all', ['sales:query', 'sales:mutation'])])
|
|
47
|
+
]);
|
|
48
|
+
expect(own).toEqual([{ taskId: 'sales.keeping', roles: ['대표'] }]);
|
|
49
|
+
});
|
|
50
|
+
it('keeps two tasks that hold the same privileges, and two that only overlap', () => {
|
|
51
|
+
const { own } = workOfRoles([
|
|
52
|
+
role('하나', [
|
|
53
|
+
task('a.one', 'all', ['a:query']),
|
|
54
|
+
task('a.two', 'all', ['a:query']),
|
|
55
|
+
task('b.one', 'all', ['b:query', 'c:query']),
|
|
56
|
+
task('b.two', 'all', ['c:query', 'd:query'])
|
|
57
|
+
])
|
|
58
|
+
]);
|
|
59
|
+
expect(ids(own)).toEqual(['a.one', 'a.two', 'b.one', 'b.two']);
|
|
60
|
+
});
|
|
61
|
+
it('says a privilege no task names once, with the roles that hold it', () => {
|
|
62
|
+
const { alone } = workOfRoles([role('하나', [], ['board:mutation']), role('둘', [], ['board:mutation', 'order:query'])]);
|
|
63
|
+
expect(alone).toEqual([
|
|
64
|
+
{ category: 'board', axis: 'mutation', roles: ['하나', '둘'] },
|
|
65
|
+
{ category: 'order', axis: 'query', roles: ['둘'] }
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=my-role-tasks.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"my-role-tasks.test.js","sourceRoot":"","sources":["../../client/components/my-role-tasks.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { workOfRoles, type MyRoleRef } from './my-role-tasks'\nimport type { TaskCoverageRef } from './role-task-edit'\n\nconst task = (taskId: string, status: TaskCoverageRef['status'], held: string[], missing: string[] = []): TaskCoverageRef => {\n const grants = (pairs: string[]) => pairs.map(pair => ({ category: pair.split(':')[0], axes: [pair.split(':')[1]] }))\n return { taskId, status, held: grants(held), missing: grants(missing) }\n}\nconst role = (name: string, tasks: TaskCoverageRef[], unclassified: string[] = []): MyRoleRef => ({\n name,\n taskCoverage: { tasks, unclassified: unclassified.map(pair => ({ category: pair.split(':')[0], axes: [pair.split(':')[1]] })) }\n})\nconst ids = (work: { taskId: string }[]) => work.map(one => one.taskId)\n\nconst self = () => task('office.self', 'all', ['office-self:query', 'office-self:mutation'])\n\ndescribe('the work my roles are said to hold', () => {\n it('★ is the tasks a role holds whole - not one held in part, nor one naming an undeclared privilege', () => {\n const { own } = workOfRoles([\n role('직원', [\n self(),\n task('office.payroll', 'none', [], ['office-payroll:query']),\n task('accounting.expense-authoring', 'some', ['counterparty-published:query'], ['accounting-self:mutation']),\n task('board.exporting', 'broken', [], ['board:query'])\n ])\n ])\n expect(ids(own)).toEqual(['office.self'])\n })\n\n it('★ says each task once across my roles, with the roles that give it', () => {\n const { own } = workOfRoles([\n role('경영지원', [self(), task('office.checks', 'all', ['office-check:query'])]),\n role('인사급여', [self(), task('office.checks', 'all', ['office-check:query']), task('office.payroll', 'all', ['office-payroll:query'])]),\n role('직원', [self()])\n ])\n expect(own).toEqual([\n { taskId: 'office.checks', roles: ['경영지원', '인사급여'] },\n { taskId: 'office.payroll', roles: ['인사급여'] }\n ])\n })\n\n it('★ says what every role of mine gives once, apart - and with one role, nothing is apart', () => {\n const two = workOfRoles([role('직원', [self()]), role('팀장', [self(), task('office.team', 'all', ['office-team:query'])])])\n expect(ids(two.shared)).toEqual(['office.self'])\n expect(ids(two.own)).toEqual(['office.team'])\n\n const one = workOfRoles([role('직원', [self()])])\n expect(ids(one.shared)).toEqual([])\n expect(ids(one.own)).toEqual(['office.self'])\n })\n\n it('★ leaves out a task whose privileges a wider one I hold already has, whichever role gives it', () => {\n const { own } = workOfRoles([\n role('경영진', [task('sales.reading', 'all', ['sales:query'])]),\n role('대표', [task('sales.keeping', 'all', ['sales:query', 'sales:mutation'])])\n ])\n expect(own).toEqual([{ taskId: 'sales.keeping', roles: ['대표'] }])\n })\n\n it('keeps two tasks that hold the same privileges, and two that only overlap', () => {\n const { own } = workOfRoles([\n role('하나', [\n task('a.one', 'all', ['a:query']),\n task('a.two', 'all', ['a:query']),\n task('b.one', 'all', ['b:query', 'c:query']),\n task('b.two', 'all', ['c:query', 'd:query'])\n ])\n ])\n expect(ids(own)).toEqual(['a.one', 'a.two', 'b.one', 'b.two'])\n })\n\n it('says a privilege no task names once, with the roles that hold it', () => {\n const { alone } = workOfRoles([role('하나', [], ['board:mutation']), role('둘', [], ['board:mutation', 'order:query'])])\n expect(alone).toEqual([\n { category: 'board', axis: 'mutation', roles: ['하나', '둘'] },\n { category: 'order', axis: 'query', roles: ['둘'] }\n ])\n })\n})\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LitElement, nothing } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* 내 역할과 업무 권한 — the roles one holds and the work they give, the body of its page (`my-roles`, ADR-0139 §11-11).
|
|
4
|
+
* Read only: a role is given by whoever manages roles, and one asks for more through an access request (ADR-0103).
|
|
5
|
+
*
|
|
6
|
+
* The roles first, each in a line; then the work, one list across them with the roles that give each; what every role
|
|
7
|
+
* gives last, once. A task is said in the words of the module that declares it (`task.<id>.name` · `.description`); a
|
|
8
|
+
* privilege no task names, by the sentence written for it (`privilege.allows.<category>.<axis>`), else by its code.
|
|
9
|
+
*/
|
|
10
|
+
export declare class MyRoles extends LitElement {
|
|
11
|
+
static styles: import("lit").CSSResult[];
|
|
12
|
+
private roles?;
|
|
13
|
+
private failed;
|
|
14
|
+
render(): import("lit-html").TemplateResult<1> | typeof nothing;
|
|
15
|
+
private renderTask;
|
|
16
|
+
firstUpdated(): void;
|
|
17
|
+
private fetch;
|
|
18
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import gql from 'graphql-tag';
|
|
3
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
4
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
5
|
+
import { client } from '@operato/graphql';
|
|
6
|
+
import { i18next } from '@operato/i18n';
|
|
7
|
+
import { store } from '@operato/shell';
|
|
8
|
+
import { workOfRoles } from './my-role-tasks.js';
|
|
9
|
+
import { axisLabel } from '../utils/privilege-axis.js';
|
|
10
|
+
/**
|
|
11
|
+
* 내 역할과 업무 권한 — the roles one holds and the work they give, the body of its page (`my-roles`, ADR-0139 §11-11).
|
|
12
|
+
* Read only: a role is given by whoever manages roles, and one asks for more through an access request (ADR-0103).
|
|
13
|
+
*
|
|
14
|
+
* The roles first, each in a line; then the work, one list across them with the roles that give each; what every role
|
|
15
|
+
* gives last, once. A task is said in the words of the module that declares it (`task.<id>.name` · `.description`); a
|
|
16
|
+
* privilege no task names, by the sentence written for it (`privilege.allows.<category>.<axis>`), else by its code.
|
|
17
|
+
*/
|
|
18
|
+
let MyRoles = class MyRoles extends LitElement {
|
|
19
|
+
constructor() {
|
|
20
|
+
super(...arguments);
|
|
21
|
+
this.failed = false;
|
|
22
|
+
}
|
|
23
|
+
static { this.styles = [
|
|
24
|
+
css `
|
|
25
|
+
:host {
|
|
26
|
+
display: block;
|
|
27
|
+
font-size: var(--ui-t-body, 13px);
|
|
28
|
+
line-height: 1.55;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
section + section {
|
|
32
|
+
margin-top: var(--spacing-large);
|
|
33
|
+
padding-top: var(--spacing-large);
|
|
34
|
+
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
h3 {
|
|
38
|
+
margin: 0 0 var(--spacing-small);
|
|
39
|
+
font-size: 0.95rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[quiet] {
|
|
43
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
p {
|
|
47
|
+
margin: 0 0 var(--spacing-small);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ul {
|
|
51
|
+
margin: 0;
|
|
52
|
+
padding: 0;
|
|
53
|
+
list-style: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
li + li {
|
|
57
|
+
margin-top: var(--spacing-medium);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
dl {
|
|
61
|
+
display: grid;
|
|
62
|
+
grid-template-columns: max-content 1fr;
|
|
63
|
+
gap: var(--spacing-small) var(--spacing-medium);
|
|
64
|
+
margin: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
dt {
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
dd {
|
|
72
|
+
margin: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[task-head] {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-wrap: wrap;
|
|
78
|
+
align-items: baseline;
|
|
79
|
+
gap: var(--spacing-small);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
[task-name] {
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[from] {
|
|
87
|
+
padding: 0 6px;
|
|
88
|
+
border-radius: 999px;
|
|
89
|
+
background-color: var(--md-sys-color-surface-variant);
|
|
90
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
91
|
+
font-size: 0.8rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
code {
|
|
95
|
+
font-size: 0.9em;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[failed] {
|
|
99
|
+
color: var(--md-sys-color-error);
|
|
100
|
+
}
|
|
101
|
+
`
|
|
102
|
+
]; }
|
|
103
|
+
render() {
|
|
104
|
+
if (this.failed)
|
|
105
|
+
return html `<div failed>${i18next.t('text.the list could not be loaded')}</div>`;
|
|
106
|
+
if (!this.roles)
|
|
107
|
+
return nothing;
|
|
108
|
+
const owner = store.getState().auth?.user?.owner;
|
|
109
|
+
if (!this.roles.length)
|
|
110
|
+
return html `<p quiet>${owner ? i18next.t('text.my roles owner') : i18next.t('text.my roles none')}</p>`;
|
|
111
|
+
const { own, shared, alone } = workOfRoles(this.roles);
|
|
112
|
+
/* Which roles give it - said only when they are not all of mine (then it would say nothing). */
|
|
113
|
+
const from = (roles) => (this.roles.length > 1 && roles.length < this.roles.length ? roles.map(name => html `<span from>${name}</span>`) : nothing);
|
|
114
|
+
return html `
|
|
115
|
+
<section>
|
|
116
|
+
<h3>${i18next.t('text.my roles count', { count: this.roles.length })}</h3>
|
|
117
|
+
${owner ? html `<p quiet>${i18next.t('text.my roles owner')}</p>` : nothing}
|
|
118
|
+
<dl>
|
|
119
|
+
${this.roles.map(role => html `<dt>${role.name}</dt><dd quiet>${role.description || ''}</dd>`)}
|
|
120
|
+
</dl>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
${own.length || alone.length
|
|
124
|
+
? html `<section>
|
|
125
|
+
<h3>${i18next.t('text.my role tasks')}</h3>
|
|
126
|
+
<ul>
|
|
127
|
+
${own.map(task => this.renderTask(task, from(task.roles)))}
|
|
128
|
+
${alone.map(one => {
|
|
129
|
+
const says = i18next.t(`privilege.allows.${one.category}.${one.axis}`, { defaultValue: '' });
|
|
130
|
+
return html `<li>
|
|
131
|
+
<div task-head>${says ? html `<span>${says}</span>` : html `<span><code>${one.category}</code> · ${axisLabel(one.axis)}</span>`} ${from(one.roles)}</div>
|
|
132
|
+
</li>`;
|
|
133
|
+
})}
|
|
134
|
+
</ul>
|
|
135
|
+
</section>`
|
|
136
|
+
: nothing}
|
|
137
|
+
|
|
138
|
+
${shared.length
|
|
139
|
+
? html `<section>
|
|
140
|
+
<h3>${i18next.t('text.my roles shared tasks')}</h3>
|
|
141
|
+
<ul>
|
|
142
|
+
${shared.map(task => this.renderTask(task, nothing))}
|
|
143
|
+
</ul>
|
|
144
|
+
</section>`
|
|
145
|
+
: nothing}
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
renderTask(task, from) {
|
|
149
|
+
const says = i18next.t(`task.${task.taskId}.description`, { defaultValue: '' });
|
|
150
|
+
return html `<li>
|
|
151
|
+
<div task-head><span task-name>${i18next.t(`task.${task.taskId}.name`, { defaultValue: task.taskId })}</span> ${from}</div>
|
|
152
|
+
${says ? html `<div quiet>${says}</div>` : nothing}
|
|
153
|
+
</li>`;
|
|
154
|
+
}
|
|
155
|
+
firstUpdated() {
|
|
156
|
+
this.fetch();
|
|
157
|
+
}
|
|
158
|
+
async fetch() {
|
|
159
|
+
try {
|
|
160
|
+
const response = await client.query({
|
|
161
|
+
query: gql `
|
|
162
|
+
query {
|
|
163
|
+
myRoles {
|
|
164
|
+
id
|
|
165
|
+
name
|
|
166
|
+
description
|
|
167
|
+
taskCoverage {
|
|
168
|
+
tasks {
|
|
169
|
+
taskId
|
|
170
|
+
status
|
|
171
|
+
held {
|
|
172
|
+
category
|
|
173
|
+
axes
|
|
174
|
+
}
|
|
175
|
+
missing {
|
|
176
|
+
category
|
|
177
|
+
axes
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
unclassified {
|
|
181
|
+
category
|
|
182
|
+
axes
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`,
|
|
188
|
+
fetchPolicy: 'no-cache'
|
|
189
|
+
});
|
|
190
|
+
if (response.errors?.length || !response.data?.myRoles)
|
|
191
|
+
throw new Error();
|
|
192
|
+
this.roles = response.data.myRoles;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
this.failed = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
__decorate([
|
|
200
|
+
state(),
|
|
201
|
+
__metadata("design:type", Array)
|
|
202
|
+
], MyRoles.prototype, "roles", void 0);
|
|
203
|
+
__decorate([
|
|
204
|
+
state(),
|
|
205
|
+
__metadata("design:type", Object)
|
|
206
|
+
], MyRoles.prototype, "failed", void 0);
|
|
207
|
+
MyRoles = __decorate([
|
|
208
|
+
customElement('my-roles')
|
|
209
|
+
], MyRoles);
|
|
210
|
+
export { MyRoles };
|
|
211
|
+
//# sourceMappingURL=my-roles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"my-roles.js","sourceRoot":"","sources":["../../client/components/my-roles.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { store } from '@operato/shell'\n\nimport { workOfRoles, type MyRoleRef, type MyWork } from './my-role-tasks.js'\nimport { axisLabel } from '../utils/privilege-axis.js'\n\ntype MyRole = MyRoleRef & { id: string; description?: string }\n\n/**\n * 내 역할과 업무 권한 — the roles one holds and the work they give, the body of its page (`my-roles`, ADR-0139 §11-11).\n * Read only: a role is given by whoever manages roles, and one asks for more through an access request (ADR-0103).\n *\n * The roles first, each in a line; then the work, one list across them with the roles that give each; what every role\n * gives last, once. A task is said in the words of the module that declares it (`task.<id>.name` · `.description`); a\n * privilege no task names, by the sentence written for it (`privilege.allows.<category>.<axis>`), else by its code.\n */\n@customElement('my-roles')\nexport class MyRoles extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n font-size: var(--ui-t-body, 13px);\n line-height: 1.55;\n }\n\n section + section {\n margin-top: var(--spacing-large);\n padding-top: var(--spacing-large);\n border-top: 1px solid var(--md-sys-color-outline-variant);\n }\n\n h3 {\n margin: 0 0 var(--spacing-small);\n font-size: 0.95rem;\n }\n\n [quiet] {\n color: var(--md-sys-color-on-surface-variant);\n }\n\n p {\n margin: 0 0 var(--spacing-small);\n }\n\n ul {\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n li + li {\n margin-top: var(--spacing-medium);\n }\n\n dl {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: var(--spacing-small) var(--spacing-medium);\n margin: 0;\n }\n\n dt {\n font-weight: 600;\n }\n\n dd {\n margin: 0;\n }\n\n [task-head] {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--spacing-small);\n }\n\n [task-name] {\n font-weight: 600;\n }\n\n [from] {\n padding: 0 6px;\n border-radius: 999px;\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n }\n\n code {\n font-size: 0.9em;\n }\n\n [failed] {\n color: var(--md-sys-color-error);\n }\n `\n ]\n\n @state() private roles?: MyRole[]\n @state() private failed = false\n\n render() {\n if (this.failed) return html`<div failed>${i18next.t('text.the list could not be loaded')}</div>`\n if (!this.roles) return nothing\n\n const owner = (store.getState() as any).auth?.user?.owner\n if (!this.roles.length) return html`<p quiet>${owner ? i18next.t('text.my roles owner') : i18next.t('text.my roles none')}</p>`\n\n const { own, shared, alone } = workOfRoles(this.roles)\n /* Which roles give it - said only when they are not all of mine (then it would say nothing). */\n const from = (roles: string[]) => (this.roles!.length > 1 && roles.length < this.roles!.length ? roles.map(name => html`<span from>${name}</span>`) : nothing)\n\n return html`\n <section>\n <h3>${i18next.t('text.my roles count', { count: this.roles.length })}</h3>\n ${owner ? html`<p quiet>${i18next.t('text.my roles owner')}</p>` : nothing}\n <dl>\n ${this.roles.map(role => html`<dt>${role.name}</dt><dd quiet>${role.description || ''}</dd>`)}\n </dl>\n </section>\n\n ${\n own.length || alone.length\n ? html`<section>\n <h3>${i18next.t('text.my role tasks')}</h3>\n <ul>\n ${own.map(task => this.renderTask(task, from(task.roles)))}\n ${alone.map(one => {\n const says = i18next.t(`privilege.allows.${one.category}.${one.axis}`, { defaultValue: '' })\n return html`<li>\n <div task-head>${says ? html`<span>${says}</span>` : html`<span><code>${one.category}</code> · ${axisLabel(one.axis)}</span>`} ${from(one.roles)}</div>\n </li>`\n })}\n </ul>\n </section>`\n : nothing\n }\n\n ${\n shared.length\n ? html`<section>\n <h3>${i18next.t('text.my roles shared tasks')}</h3>\n <ul>\n ${shared.map(task => this.renderTask(task, nothing))}\n </ul>\n </section>`\n : nothing\n }\n `\n }\n\n private renderTask(task: MyWork, from: unknown) {\n const says = i18next.t(`task.${task.taskId}.description`, { defaultValue: '' })\n return html`<li>\n <div task-head><span task-name>${i18next.t(`task.${task.taskId}.name`, { defaultValue: task.taskId })}</span> ${from}</div>\n ${says ? html`<div quiet>${says}</div>` : nothing}\n </li>`\n }\n\n firstUpdated() {\n this.fetch()\n }\n\n private async fetch() {\n try {\n const response = await client.query({\n query: gql`\n query {\n myRoles {\n id\n name\n description\n taskCoverage {\n tasks {\n taskId\n status\n held {\n category\n axes\n }\n missing {\n category\n axes\n }\n }\n unclassified {\n category\n axes\n }\n }\n }\n }\n `,\n fetchPolicy: 'no-cache'\n })\n if (response.errors?.length || !response.data?.myRoles) throw new Error()\n this.roles = response.data.myRoles\n } catch {\n this.failed = true\n }\n }\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '@material/web/icon/icon.js';
|
|
2
2
|
import '@operato/data-grist';
|
|
3
|
+
import '@operato/input/ox-input-select-buttons.js';
|
|
3
4
|
import type { CSSResultGroup } from 'lit';
|
|
4
5
|
import { PageView } from '@operato/shell';
|
|
5
6
|
import type { FetchOption } from '@operato/data-grist';
|
|
@@ -19,6 +20,8 @@ export declare class AuditPage extends AuditPageBase {
|
|
|
19
20
|
private actor?;
|
|
20
21
|
private door?;
|
|
21
22
|
private outcome;
|
|
23
|
+
/** How many records each outcome chip would show under the other conditions, counted by the records' own door. */
|
|
24
|
+
private outcomeCounts;
|
|
22
25
|
private health?;
|
|
23
26
|
private grist;
|
|
24
27
|
get context(): {
|
|
@@ -34,6 +37,7 @@ export declare class AuditPage extends AuditPageBase {
|
|
|
34
37
|
private show;
|
|
35
38
|
private lastDays;
|
|
36
39
|
private pick;
|
|
40
|
+
private countOutcomes;
|
|
37
41
|
private narrow;
|
|
38
42
|
pageUpdated(_changes: any, _lifecycle: any): Promise<void>;
|
|
39
43
|
private readHealth;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import '@material/web/icon/icon.js';
|
|
3
3
|
import '@operato/data-grist';
|
|
4
|
+
import '@operato/input/ox-input-select-buttons.js';
|
|
4
5
|
import gql from 'graphql-tag';
|
|
5
6
|
import { css, html, nothing } from 'lit';
|
|
6
7
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
@@ -31,6 +32,8 @@ let AuditPage = class AuditPage extends AuditPageBase {
|
|
|
31
32
|
this.from = lastDays(30).from;
|
|
32
33
|
this.to = new Date();
|
|
33
34
|
this.outcome = '';
|
|
35
|
+
/** How many records each outcome chip would show under the other conditions, counted by the records' own door. */
|
|
36
|
+
this.outcomeCounts = new Map();
|
|
34
37
|
}
|
|
35
38
|
static { this.styles = [
|
|
36
39
|
WorkSurfacePageStyles,
|
|
@@ -49,11 +52,6 @@ let AuditPage = class AuditPage extends AuditPageBase {
|
|
|
49
52
|
gap: var(--spacing-tiny, 2px);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
button[aria-pressed='true'] {
|
|
53
|
-
background-color: var(--md-sys-color-secondary-container);
|
|
54
|
-
color: var(--md-sys-color-on-secondary-container);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
55
|
input[type='date'] {
|
|
58
56
|
height: var(--context-action-height, 28px);
|
|
59
57
|
box-sizing: border-box;
|
|
@@ -99,23 +97,41 @@ let AuditPage = class AuditPage extends AuditPageBase {
|
|
|
99
97
|
}
|
|
100
98
|
renderControls() {
|
|
101
99
|
const t = i18next.t.bind(i18next);
|
|
102
|
-
|
|
100
|
+
/* One of each is always chosen: a press on the chosen chip leaves it chosen. */
|
|
101
|
+
const choose = (e, chosen, pick) => {
|
|
102
|
+
if (e.detail)
|
|
103
|
+
pick(e.detail);
|
|
104
|
+
else
|
|
105
|
+
e.target.value = chosen;
|
|
106
|
+
};
|
|
103
107
|
return html `
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
<ox-input-select-buttons
|
|
109
|
+
.options=${VIEWS.map(view => ({ value: view, display: t(`audit.view.${view}`) }))}
|
|
110
|
+
.value=${this.view}
|
|
111
|
+
@change=${(e) => choose(e, this.view, view => this.show(view))}
|
|
112
|
+
></ox-input-select-buttons>
|
|
107
113
|
|
|
108
114
|
<div group>
|
|
109
|
-
|
|
115
|
+
<ox-input-select-buttons
|
|
116
|
+
.options=${PERIODS.map(days => ({ value: String(days), display: t('audit.period.days', { count: days }) }))}
|
|
117
|
+
.value=${this.days ? String(this.days) : undefined}
|
|
118
|
+
@change=${(e) => choose(e, this.days ? String(this.days) : undefined, days => this.lastDays(Number(days)))}
|
|
119
|
+
></ox-input-select-buttons>
|
|
110
120
|
<input type="date" aria-label=${t('audit.period.from')} .value=${dateInputValue(this.from)} @change=${(e) => this.pick('from', e)} />
|
|
111
121
|
<span>~</span>
|
|
112
122
|
<input type="date" aria-label=${t('audit.period.to')} .value=${dateInputValue(this.to)} @change=${(e) => this.pick('to', e)} />
|
|
113
123
|
</div>
|
|
114
124
|
|
|
115
125
|
${this.view === 'records'
|
|
116
|
-
? html `<
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
? html `<ox-input-select-buttons
|
|
127
|
+
.options=${OUTCOMES.map(outcome => ({
|
|
128
|
+
value: outcome || 'all',
|
|
129
|
+
display: t(outcome ? `audit.outcome.${outcome}` : 'audit.outcome.all'),
|
|
130
|
+
count: this.outcomeCounts.get(outcome || 'all') ?? 0
|
|
131
|
+
}))}
|
|
132
|
+
.value=${this.outcome || 'all'}
|
|
133
|
+
@change=${(e) => choose(e, this.outcome || 'all', outcome => this.narrow({ outcome: (outcome === 'all' ? '' : outcome) }))}
|
|
134
|
+
></ox-input-select-buttons>`
|
|
119
135
|
: nothing}
|
|
120
136
|
${this.view === 'records' && this.actor
|
|
121
137
|
? html `<span narrowed>${this.actor.name}<button quiet small @click=${() => this.narrow({ actor: undefined })}>${t('audit.button.all-people')}</button></span>`
|
|
@@ -154,6 +170,24 @@ let AuditPage = class AuditPage extends AuditPageBase {
|
|
|
154
170
|
this.days = null;
|
|
155
171
|
this.grist?.fetch();
|
|
156
172
|
}
|
|
173
|
+
/*
|
|
174
|
+
* The outcome chips' numbers - the records' own door asked for its total once per outcome, under the period, the
|
|
175
|
+
* person and the privilege narrowed to, in one request.
|
|
176
|
+
*/
|
|
177
|
+
async countOutcomes(filter) {
|
|
178
|
+
const response = await client.query({
|
|
179
|
+
query: gql `
|
|
180
|
+
query (${OUTCOMES.map((_, at) => `$f${at}: AuditRecordFilter!`).join(', ')}) {
|
|
181
|
+
${OUTCOMES.map((_, at) => `c${at}: auditRecords(filter: $f${at}) { total }`).join('\n')}
|
|
182
|
+
}
|
|
183
|
+
`,
|
|
184
|
+
variables: Object.fromEntries(OUTCOMES.map((outcome, at) => [`f${at}`, { ...filter, outcome: outcome || undefined, page: 1, limit: 1 }])),
|
|
185
|
+
fetchPolicy: 'no-cache'
|
|
186
|
+
});
|
|
187
|
+
if (response.errors?.length)
|
|
188
|
+
return console.error(response.errors);
|
|
189
|
+
this.outcomeCounts = new Map(OUTCOMES.map((outcome, at) => [outcome || 'all', response.data?.[`c${at}`]?.total ?? 0]));
|
|
190
|
+
}
|
|
157
191
|
narrow(change) {
|
|
158
192
|
Object.assign(this, change);
|
|
159
193
|
if (this.view !== 'records')
|
|
@@ -308,6 +342,7 @@ let AuditPage = class AuditPage extends AuditPageBase {
|
|
|
308
342
|
.sort((a, b) => used(b) - used(a));
|
|
309
343
|
return { total: records.length, records };
|
|
310
344
|
}
|
|
345
|
+
this.countOutcomes({ ...period, actorId: this.actor?.id, door: this.door });
|
|
311
346
|
const response = await client.query({
|
|
312
347
|
query: gql `
|
|
313
348
|
query ($filter: AuditRecordFilter!) {
|
|
@@ -403,6 +438,10 @@ __decorate([
|
|
|
403
438
|
state(),
|
|
404
439
|
__metadata("design:type", String)
|
|
405
440
|
], AuditPage.prototype, "outcome", void 0);
|
|
441
|
+
__decorate([
|
|
442
|
+
state(),
|
|
443
|
+
__metadata("design:type", Object)
|
|
444
|
+
], AuditPage.prototype, "outcomeCounts", void 0);
|
|
406
445
|
__decorate([
|
|
407
446
|
state(),
|
|
408
447
|
__metadata("design:type", Object)
|