@things-factory/auth-ui 10.1.198 → 10.1.203

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 (34) hide show
  1. package/dist-client/components/access-decision-popup.js +7 -5
  2. package/dist-client/components/access-decision-popup.js.map +1 -1
  3. package/dist-client/components/access-request-popup.d.ts +9 -1
  4. package/dist-client/components/access-request-popup.js +55 -4
  5. package/dist-client/components/access-request-popup.js.map +1 -1
  6. package/dist-client/components/role-task-edit.d.ts +22 -0
  7. package/dist-client/components/role-task-edit.js +17 -0
  8. package/dist-client/components/role-task-edit.js.map +1 -0
  9. package/dist-client/components/role-task-edit.test.d.ts +1 -0
  10. package/dist-client/components/role-task-edit.test.js +34 -0
  11. package/dist-client/components/role-task-edit.test.js.map +1 -0
  12. package/dist-client/pages/access-request/access-requests.js +4 -2
  13. package/dist-client/pages/access-request/access-requests.js.map +1 -1
  14. package/dist-client/pages/access-request/my-access-requests.js +3 -2
  15. package/dist-client/pages/access-request/my-access-requests.js.map +1 -1
  16. package/dist-client/pages/role/role-privilege-page.d.ts +10 -2
  17. package/dist-client/pages/role/role-privilege-page.js +62 -3
  18. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  19. package/dist-client/pages/role/role-task-review.test.js +13 -2
  20. package/dist-client/pages/role/role-task-review.test.js.map +1 -1
  21. package/dist-client/tsconfig.tsbuildinfo +1 -1
  22. package/dist-client/utils/access-request.d.ts +15 -1
  23. package/dist-client/utils/access-request.js +19 -2
  24. package/dist-client/utils/access-request.js.map +1 -1
  25. package/dist-client/utils/access-request.test.js +26 -1
  26. package/dist-client/utils/access-request.test.js.map +1 -1
  27. package/dist-server/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +4 -4
  29. package/translations/en.json +11 -1
  30. package/translations/es.json +11 -1
  31. package/translations/ja.json +11 -1
  32. package/translations/ko.json +11 -1
  33. package/translations/ms.json +11 -1
  34. package/translations/zh.json +11 -1
@@ -10,7 +10,7 @@ import { customElement, property, state } from 'lit/decorators.js';
10
10
  import { client, gqlContext } from '@operato/graphql';
11
11
  import { i18next, localize } from '@operato/i18n';
12
12
  import { CommonPopupStyles, ScrollbarStyles } from '@operato/styles';
13
- import { decisionButtons, firstCandidate, issueWords, orderedIssues, privilegeWords } from '../utils/access-request.js';
13
+ import { decisionButtons, firstCandidate, issueWords, neededWords, orderedIssues, taskSays } from '../utils/access-request.js';
14
14
  const REVIEW = gql `
15
15
  query ($decision: AccessDecisionInput!) {
16
16
  reviewAccessDecision(decision: $decision) {
@@ -89,15 +89,16 @@ let AccessDecisionPopup = class AccessDecisionPopup extends localize(i18next)(Li
89
89
  async firstUpdated() {
90
90
  const response = await client.query({
91
91
  query: gql `
92
- query ($category: String!, $privilege: String!) {
93
- rolesGranting(category: $category, privilege: $privilege) {
92
+ query ($category: String!, $privilege: String!, $taskId: String) {
93
+ rolesGranting(category: $category, privilege: $privilege, taskId: $taskId) {
94
94
  id
95
95
  name
96
96
  privilegeCount
97
+ holdsTask
97
98
  }
98
99
  }
99
100
  `,
100
- variables: { category: this.request.category, privilege: this.request.privilege },
101
+ variables: { category: this.request.category, privilege: this.request.privilege, taskId: this.request.taskId || null },
101
102
  context: gqlContext(),
102
103
  fetchPolicy: 'network-only'
103
104
  });
@@ -126,7 +127,8 @@ let AccessDecisionPopup = class AccessDecisionPopup extends localize(i18next)(Li
126
127
  return html `
127
128
  <div body>
128
129
  <div who>${t('access-request.text.who asks', { name: request.requester?.name || request.requester?.email || '' })}</div>
129
- <div needed>${privilegeWords(t, request.category, request.privilege)}</div>
130
+ <div needed>${neededWords(t, request)}</div>
131
+ ${request.taskId && taskSays(t, request.taskId) ? html `<div who>${taskSays(t, request.taskId)}</div>` : nothing}
130
132
  ${request.reason ? html `<div why>${request.reason}</div>` : nothing}
131
133
 
132
134
  <div>${t('access-request.text.grant with role')}</div>
@@ -1 +1 @@
1
- {"version":3,"file":"access-decision-popup.js","sourceRoot":"","sources":["../../client/components/access-decision-popup.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/popup/ox-popup-actions.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/radio/radio.js'\nimport '@material/web/textfield/outlined-text-field.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { CommonPopupStyles, ScrollbarStyles } from '@operato/styles'\n\nimport { decisionButtons, firstCandidate, issueWords, orderedIssues, privilegeWords, type ReviewIssue } from '../utils/access-request.js'\n\nconst REVIEW = gql`\n query ($decision: AccessDecisionInput!) {\n reviewAccessDecision(decision: $decision) {\n code\n blocking\n field\n params\n }\n }\n`\n\n/**\n * Deciding one access request (ADR-0103 · docs/design/access-request.md §6-1).\n *\n * The findings are the server's review - the same function the AI draft card and the decision door read - so\n * this popup never judges a role itself. Each change of role or reason asks again. A blocking finding takes\n * the button away; a warning stays beside it.\n */\n@customElement('access-decision-popup')\nexport class AccessDecisionPopup extends localize(i18next)(LitElement) {\n static styles = [\n CommonPopupStyles,\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--md-sys-color-surface);\n }\n [body] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium);\n padding: var(--spacing-large);\n }\n [who],\n [why] {\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface-variant);\n }\n [needed] {\n font: var(--title-font, inherit);\n }\n label[candidate] {\n display: flex;\n align-items: center;\n gap: var(--spacing-small);\n }\n [count] {\n color: var(--md-sys-color-on-surface-variant);\n }\n [issue][blocking] {\n color: var(--md-sys-color-error);\n }\n [issue]:not([blocking]) {\n color: var(--md-sys-color-tertiary);\n }\n [none] {\n color: var(--md-sys-color-on-surface-variant);\n }\n md-outlined-text-field {\n width: 100%;\n }\n `\n ]\n\n @property({ type: Object }) request: any\n\n @state() private candidates: { id: string; name: string; privilegeCount: number }[] = []\n @state() private roleId?: string\n @state() private reason = ''\n @state() private grantIssues: ReviewIssue[] | null = null\n @state() private denyIssues: ReviewIssue[] | null = null\n\n private asked = 0\n\n async firstUpdated() {\n const response = await client.query({\n query: gql`\n query ($category: String!, $privilege: String!) {\n rolesGranting(category: $category, privilege: $privilege) {\n id\n name\n privilegeCount\n }\n }\n `,\n variables: { category: this.request.category, privilege: this.request.privilege },\n context: gqlContext(),\n fetchPolicy: 'network-only'\n })\n this.candidates = response.data?.rolesGranting || []\n this.roleId = firstCandidate(this.candidates)?.id\n await this.review()\n }\n\n /** Asks the server again; an answer to an older question is dropped. */\n private async review() {\n const asked = ++this.asked\n const ask = (decision: any) => client.query({ query: REVIEW, variables: { decision }, context: gqlContext(), fetchPolicy: 'network-only' })\n const [grant, deny] = await Promise.all([\n ask({ requestId: this.request.id, decision: 'GRANT', roleId: this.roleId }),\n ask({ requestId: this.request.id, decision: 'DENY', reason: this.reason })\n ])\n if (asked !== this.asked) return\n this.grantIssues = grant.errors?.length ? null : grant.data?.reviewAccessDecision || []\n this.denyIssues = deny.errors?.length ? null : deny.data?.reviewAccessDecision || []\n }\n\n render() {\n const t = i18next.t.bind(i18next)\n const { request } = this\n const buttons = decisionButtons(this.grantIssues, this.denyIssues)\n const reasonIssues = (this.denyIssues || []).filter(issue => issue.field === 'reason')\n\n return html`\n <div body>\n <div who>${t('access-request.text.who asks', { name: request.requester?.name || request.requester?.email || '' })}</div>\n <div needed>${privilegeWords(t, request.category, request.privilege)}</div>\n ${request.reason ? html`<div why>${request.reason}</div>` : nothing}\n\n <div>${t('access-request.text.grant with role')}</div>\n ${this.candidates.length === 0\n ? html`<div none>${t('access-request.text.no role grants it')}</div>`\n : this.candidates.map(\n candidate => html`\n <label candidate>\n <md-radio\n name=\"role\"\n .checked=${candidate.id === this.roleId}\n @change=${() => {\n this.roleId = candidate.id\n this.review()\n }}\n ></md-radio>\n <span>${candidate.name}</span>\n <span count>${t('access-request.text.privilege count', { count: candidate.privilegeCount })}</span>\n </label>\n `\n )}\n ${orderedIssues(this.grantIssues || []).map(issue => html`<div issue ?blocking=${issue.blocking}>${issueWords(t, issue)}</div>`)}\n\n <md-outlined-text-field\n label=${t('access-request.field.deny reason')}\n type=\"textarea\"\n rows=\"2\"\n .value=${this.reason}\n @input=${(e: any) => {\n this.reason = e.target.value\n this.review()\n }}\n ></md-outlined-text-field>\n ${reasonIssues.length && this.reason ? reasonIssues.map(issue => html`<div issue blocking>${issueWords(t, issue)}</div>`) : nothing}\n </div>\n\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => history.back()}>${t('button.cancel')}</md-text-button>\n <md-text-button slot=\"start\" ?disabled=${!buttons.deny} @click=${() => this.decide('DENY')}>\n ${t('access-request.button.deny')}\n </md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${!buttons.grant} @click=${() => this.decide('GRANT')}>\n ${t('access-request.button.grant')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n private async decide(decision: 'GRANT' | 'DENY') {\n const response = await client.mutate({\n mutation: gql`\n mutation ($decision: AccessDecisionInput!) {\n decideAccessRequest(decision: $decision) {\n id\n state\n }\n }\n `,\n variables: {\n decision:\n decision === 'GRANT'\n ? { requestId: this.request.id, decision, roleId: this.roleId }\n : { requestId: this.request.id, decision, reason: this.reason }\n },\n context: gqlContext()\n })\n\n if (!response.errors?.length) {\n this.dispatchEvent(new CustomEvent('access-request-decided', { bubbles: true, composed: true }))\n history.back()\n } else {\n /* Somebody may have decided meanwhile - show what the server says now. */\n await this.review()\n }\n }\n}\n"]}
1
+ {"version":3,"file":"access-decision-popup.js","sourceRoot":"","sources":["../../client/components/access-decision-popup.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/popup/ox-popup-actions.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/radio/radio.js'\nimport '@material/web/textfield/outlined-text-field.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { CommonPopupStyles, ScrollbarStyles } from '@operato/styles'\n\nimport { decisionButtons, firstCandidate, issueWords, neededWords, orderedIssues, taskSays, type ReviewIssue } from '../utils/access-request.js'\n\nconst REVIEW = gql`\n query ($decision: AccessDecisionInput!) {\n reviewAccessDecision(decision: $decision) {\n code\n blocking\n field\n params\n }\n }\n`\n\n/**\n * Deciding one access request (ADR-0103 · docs/design/access-request.md §6-1).\n *\n * The findings are the server's review - the same function the AI draft card and the decision door read - so\n * this popup never judges a role itself. Each change of role or reason asks again. A blocking finding takes\n * the button away; a warning stays beside it.\n */\n@customElement('access-decision-popup')\nexport class AccessDecisionPopup extends localize(i18next)(LitElement) {\n static styles = [\n CommonPopupStyles,\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--md-sys-color-surface);\n }\n [body] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium);\n padding: var(--spacing-large);\n }\n [who],\n [why] {\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface-variant);\n }\n [needed] {\n font: var(--title-font, inherit);\n }\n label[candidate] {\n display: flex;\n align-items: center;\n gap: var(--spacing-small);\n }\n [count] {\n color: var(--md-sys-color-on-surface-variant);\n }\n [issue][blocking] {\n color: var(--md-sys-color-error);\n }\n [issue]:not([blocking]) {\n color: var(--md-sys-color-tertiary);\n }\n [none] {\n color: var(--md-sys-color-on-surface-variant);\n }\n md-outlined-text-field {\n width: 100%;\n }\n `\n ]\n\n @property({ type: Object }) request: any\n\n @state() private candidates: { id: string; name: string; privilegeCount: number; holdsTask: boolean }[] = []\n @state() private roleId?: string\n @state() private reason = ''\n @state() private grantIssues: ReviewIssue[] | null = null\n @state() private denyIssues: ReviewIssue[] | null = null\n\n private asked = 0\n\n async firstUpdated() {\n const response = await client.query({\n query: gql`\n query ($category: String!, $privilege: String!, $taskId: String) {\n rolesGranting(category: $category, privilege: $privilege, taskId: $taskId) {\n id\n name\n privilegeCount\n holdsTask\n }\n }\n `,\n variables: { category: this.request.category, privilege: this.request.privilege, taskId: this.request.taskId || null },\n context: gqlContext(),\n fetchPolicy: 'network-only'\n })\n this.candidates = response.data?.rolesGranting || []\n this.roleId = firstCandidate(this.candidates)?.id\n await this.review()\n }\n\n /** Asks the server again; an answer to an older question is dropped. */\n private async review() {\n const asked = ++this.asked\n const ask = (decision: any) => client.query({ query: REVIEW, variables: { decision }, context: gqlContext(), fetchPolicy: 'network-only' })\n const [grant, deny] = await Promise.all([\n ask({ requestId: this.request.id, decision: 'GRANT', roleId: this.roleId }),\n ask({ requestId: this.request.id, decision: 'DENY', reason: this.reason })\n ])\n if (asked !== this.asked) return\n this.grantIssues = grant.errors?.length ? null : grant.data?.reviewAccessDecision || []\n this.denyIssues = deny.errors?.length ? null : deny.data?.reviewAccessDecision || []\n }\n\n render() {\n const t = i18next.t.bind(i18next)\n const { request } = this\n const buttons = decisionButtons(this.grantIssues, this.denyIssues)\n const reasonIssues = (this.denyIssues || []).filter(issue => issue.field === 'reason')\n\n return html`\n <div body>\n <div who>${t('access-request.text.who asks', { name: request.requester?.name || request.requester?.email || '' })}</div>\n <div needed>${neededWords(t, request)}</div>\n ${request.taskId && taskSays(t, request.taskId) ? html`<div who>${taskSays(t, request.taskId)}</div>` : nothing}\n ${request.reason ? html`<div why>${request.reason}</div>` : nothing}\n\n <div>${t('access-request.text.grant with role')}</div>\n ${this.candidates.length === 0\n ? html`<div none>${t('access-request.text.no role grants it')}</div>`\n : this.candidates.map(\n candidate => html`\n <label candidate>\n <md-radio\n name=\"role\"\n .checked=${candidate.id === this.roleId}\n @change=${() => {\n this.roleId = candidate.id\n this.review()\n }}\n ></md-radio>\n <span>${candidate.name}</span>\n <span count>${t('access-request.text.privilege count', { count: candidate.privilegeCount })}</span>\n </label>\n `\n )}\n ${orderedIssues(this.grantIssues || []).map(issue => html`<div issue ?blocking=${issue.blocking}>${issueWords(t, issue)}</div>`)}\n\n <md-outlined-text-field\n label=${t('access-request.field.deny reason')}\n type=\"textarea\"\n rows=\"2\"\n .value=${this.reason}\n @input=${(e: any) => {\n this.reason = e.target.value\n this.review()\n }}\n ></md-outlined-text-field>\n ${reasonIssues.length && this.reason ? reasonIssues.map(issue => html`<div issue blocking>${issueWords(t, issue)}</div>`) : nothing}\n </div>\n\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => history.back()}>${t('button.cancel')}</md-text-button>\n <md-text-button slot=\"start\" ?disabled=${!buttons.deny} @click=${() => this.decide('DENY')}>\n ${t('access-request.button.deny')}\n </md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${!buttons.grant} @click=${() => this.decide('GRANT')}>\n ${t('access-request.button.grant')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n private async decide(decision: 'GRANT' | 'DENY') {\n const response = await client.mutate({\n mutation: gql`\n mutation ($decision: AccessDecisionInput!) {\n decideAccessRequest(decision: $decision) {\n id\n state\n }\n }\n `,\n variables: {\n decision:\n decision === 'GRANT'\n ? { requestId: this.request.id, decision, roleId: this.roleId }\n : { requestId: this.request.id, decision, reason: this.reason }\n },\n context: gqlContext()\n })\n\n if (!response.errors?.length) {\n this.dispatchEvent(new CustomEvent('access-request-decided', { bubbles: true, composed: true }))\n history.back()\n } else {\n /* Somebody may have decided meanwhile - show what the server says now. */\n await this.review()\n }\n }\n}\n"]}
@@ -3,11 +3,14 @@ import '@material/web/button/text-button.js';
3
3
  import '@material/web/button/filled-button.js';
4
4
  import '@material/web/textfield/outlined-text-field.js';
5
5
  import { LitElement } from 'lit';
6
+ import '@material/web/radio/radio.js';
6
7
  declare const AccessRequestPopup_base: (new (...args: any[]) => LitElement) & typeof LitElement;
7
8
  /**
8
9
  * Asking for a privilege the person was just refused (ADR-0103, docs/design/access-request.md §3-2).
9
10
  *
10
- * What was needed comes from the refusal - the person writes only why, and must (questions Q9). Before sending
11
+ * What was needed comes from the refusal - the person writes only why, and must (questions Q9). It is said as the work
12
+ * it belongs to: every task holding the refused privilege is listed, fewest privileges first, the first picked, and the
13
+ * person picks the one they were doing (ADR-0103 decision 9). A privilege no task holds is said as itself. Before sending
11
14
  * the popup says the request is recorded and how many people it reaches; a denial of the last days is shown with
12
15
  * its reason, and the same words are not sent again. A request already waiting is shown instead of a blank form:
13
16
  * pressing again adds to it, the people deciding are not told twice.
@@ -25,8 +28,13 @@ export declare class AccessRequestPopup extends AccessRequestPopup_base {
25
28
  private deciders;
26
29
  private reason;
27
30
  private sending;
31
+ /** The tasks that hold the refused privilege, fewest privileges first - the server's order (tasksGranting). */
32
+ private tasks;
33
+ private taskId?;
28
34
  firstUpdated(): Promise<void>;
29
35
  render(): import("lit-html").TemplateResult<1>;
36
+ /** The work the refused privilege belongs to - one line for one task, a choice for several, the privilege for none. */
37
+ private renderNeeded;
30
38
  private send;
31
39
  }
32
40
  export {};
@@ -9,11 +9,14 @@ import { customElement, property, state } from 'lit/decorators.js';
9
9
  import { client, gqlContext } from '@operato/graphql';
10
10
  import { i18next, localize } from '@operato/i18n';
11
11
  import { CommonPopupStyles } from '@operato/styles';
12
- import { maySend, privilegeWords, recentDenial } from '../utils/access-request.js';
12
+ import '@material/web/radio/radio.js';
13
+ import { maySend, privilegeWords, recentDenial, taskName, taskSays } from '../utils/access-request.js';
13
14
  /**
14
15
  * Asking for a privilege the person was just refused (ADR-0103, docs/design/access-request.md §3-2).
15
16
  *
16
- * What was needed comes from the refusal - the person writes only why, and must (questions Q9). Before sending
17
+ * What was needed comes from the refusal - the person writes only why, and must (questions Q9). It is said as the work
18
+ * it belongs to: every task holding the refused privilege is listed, fewest privileges first, the first picked, and the
19
+ * person picks the one they were doing (ADR-0103 decision 9). A privilege no task holds is said as itself. Before sending
17
20
  * the popup says the request is recorded and how many people it reaches; a denial of the last days is shown with
18
21
  * its reason, and the same words are not sent again. A request already waiting is shown instead of a blank form:
19
22
  * pressing again adds to it, the people deciding are not told twice.
@@ -31,6 +34,8 @@ let AccessRequestPopup = class AccessRequestPopup extends localize(i18next)(LitE
31
34
  this.deciders = null;
32
35
  this.reason = '';
33
36
  this.sending = false;
37
+ /** The tasks that hold the refused privilege, fewest privileges first - the server's order (tasksGranting). */
38
+ this.tasks = [];
34
39
  }
35
40
  static { this.styles = [
36
41
  CommonPopupStyles,
@@ -55,6 +60,15 @@ let AccessRequestPopup = class AccessRequestPopup extends localize(i18next)(LitE
55
60
  font: var(--title-font, inherit);
56
61
  color: var(--md-sys-color-on-surface);
57
62
  }
63
+ label[task] {
64
+ display: flex;
65
+ align-items: flex-start;
66
+ gap: var(--spacing-small);
67
+ }
68
+ [says] {
69
+ font: var(--label-font, inherit);
70
+ color: var(--md-sys-color-on-surface-variant);
71
+ }
58
72
  [operation],
59
73
  [waiting],
60
74
  [recorded] {
@@ -73,11 +87,12 @@ let AccessRequestPopup = class AccessRequestPopup extends localize(i18next)(LitE
73
87
  async firstUpdated() {
74
88
  const response = await client.query({
75
89
  query: gql `
76
- query {
90
+ query ($category: String!, $privilege: String!) {
77
91
  myAccessRequests {
78
92
  id
79
93
  category
80
94
  privilege
95
+ taskId
81
96
  state
82
97
  reason
83
98
  count
@@ -86,8 +101,12 @@ let AccessRequestPopup = class AccessRequestPopup extends localize(i18next)(LitE
86
101
  decisionReason
87
102
  }
88
103
  accessRequestDeciderCount
104
+ tasksGranting(category: $category, privilege: $privilege) {
105
+ taskId
106
+ }
89
107
  }
90
108
  `,
109
+ variables: { category: this.category, privilege: this.privilege },
91
110
  context: gqlContext(),
92
111
  fetchPolicy: 'network-only'
93
112
  });
@@ -96,11 +115,14 @@ let AccessRequestPopup = class AccessRequestPopup extends localize(i18next)(LitE
96
115
  this.denied = this.waiting ? null : recentDenial(mine, this.category, this.privilege) || null;
97
116
  this.deciders = typeof response.data?.accessRequestDeciderCount === 'number' ? response.data.accessRequestDeciderCount : null;
98
117
  this.reason = this.waiting?.reason || '';
118
+ this.tasks = response.data?.tasksGranting || [];
119
+ /* A request already waiting keeps the work it named - asking again adds to it and does not change it. */
120
+ this.taskId = this.tasks.find(task => task.taskId === this.waiting?.taskId)?.taskId ?? this.tasks[0]?.taskId;
99
121
  }
100
122
  render() {
101
123
  return html `
102
124
  <div body>
103
- <div needed>${privilegeWords(i18next.t.bind(i18next), this.category, this.privilege)}</div>
125
+ ${this.renderNeeded()}
104
126
  ${this.operationName
105
127
  ? html `<div operation>${i18next.t('access-request.text.refused at', { operation: this.operationName })}</div>`
106
128
  : ''}
@@ -142,6 +164,26 @@ let AccessRequestPopup = class AccessRequestPopup extends localize(i18next)(LitE
142
164
  </ox-popup-actions>
143
165
  `;
144
166
  }
167
+ /** The work the refused privilege belongs to - one line for one task, a choice for several, the privilege for none. */
168
+ renderNeeded() {
169
+ const t = i18next.t.bind(i18next);
170
+ if (this.tasks.length === 0)
171
+ return html `<div needed>${privilegeWords(t, this.category, this.privilege)}</div>`;
172
+ if (this.tasks.length === 1) {
173
+ const says = taskSays(t, this.tasks[0].taskId);
174
+ return html `<div needed>${taskName(t, this.tasks[0].taskId)}</div>
175
+ ${says ? html `<div says>${says}</div>` : ''}`;
176
+ }
177
+ return this.tasks.map(task => html `
178
+ <label task>
179
+ <md-radio name="task" .checked=${task.taskId === this.taskId} @change=${() => (this.taskId = task.taskId)}></md-radio>
180
+ <span>
181
+ <div needed>${taskName(t, task.taskId)}</div>
182
+ ${taskSays(t, task.taskId) ? html `<div says>${taskSays(t, task.taskId)}</div>` : ''}
183
+ </span>
184
+ </label>
185
+ `);
186
+ }
145
187
  async send() {
146
188
  this.sending = true;
147
189
  try {
@@ -159,6 +201,7 @@ let AccessRequestPopup = class AccessRequestPopup extends localize(i18next)(LitE
159
201
  category: this.category,
160
202
  privilege: this.privilege,
161
203
  operationName: this.operationName,
204
+ taskId: this.taskId,
162
205
  reason: this.reason.trim()
163
206
  }
164
207
  },
@@ -209,6 +252,14 @@ __decorate([
209
252
  state(),
210
253
  __metadata("design:type", Object)
211
254
  ], AccessRequestPopup.prototype, "sending", void 0);
255
+ __decorate([
256
+ state(),
257
+ __metadata("design:type", Array)
258
+ ], AccessRequestPopup.prototype, "tasks", void 0);
259
+ __decorate([
260
+ state(),
261
+ __metadata("design:type", String)
262
+ ], AccessRequestPopup.prototype, "taskId", void 0);
212
263
  AccessRequestPopup = __decorate([
213
264
  customElement('access-request-popup')
214
265
  ], AccessRequestPopup);
@@ -1 +1 @@
1
- {"version":3,"file":"access-request-popup.js","sourceRoot":"","sources":["../../client/components/access-request-popup.ts"],"names":[],"mappings":";AAAA,OAAO,oCAAoC,CAAA;AAC3C,OAAO,qCAAqC,CAAA;AAC5C,OAAO,uCAAuC,CAAA;AAC9C,OAAO,gDAAgD,CAAA;AAEvD,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAEnD,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAElF;;;;;;;GAOG;AAEI,IAAM,kBAAkB,GAAxB,MAAM,kBAAmB,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAA9D;;QAwCuB,aAAQ,GAAW,EAAE,CAAA;QACrB,cAAS,GAAW,EAAE,CAAA;QAGlD,8EAA8E;QAC7D,YAAO,GAAQ,IAAI,CAAA;QACpC,+GAA+G;QAC9F,WAAM,GAAQ,IAAI,CAAA;QACnC,wEAAwE;QACvD,aAAQ,GAAkB,IAAI,CAAA;QAC9B,WAAM,GAAG,EAAE,CAAA;QACX,YAAO,GAAG,KAAK,CAAA;IAgHlC,CAAC;aAlKQ,WAAM,GAAG;QACd,iBAAiB;QACjB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAkCF;KACF,AArCY,CAqCZ;IAeD,KAAK,CAAC,YAAY;QAChB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;OAeT;YACD,OAAO,EAAE,UAAU,EAAE;YACrB,WAAW,EAAE,cAAc;SAC5B,CAAC,CAAA;QACF,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,gBAAgB,IAAI,EAAE,CAAA;QAClD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAA;QACrI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAA;QAC7F,IAAI,CAAC,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,EAAE,yBAAyB,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAA;QAC7H,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,CAAA;IAC1C,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;;sBAEO,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC;UAClF,IAAI,CAAC,aAAa;YAClB,CAAC,CAAC,IAAI,CAAA,kBAAkB,OAAO,CAAC,CAAC,CAAC,gCAAgC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,QAAQ;YAC9G,CAAC,CAAC,EAAE;UACJ,IAAI,CAAC,OAAO;YACZ,CAAC,CAAC,IAAI,CAAA;gBACA,OAAO,CAAC,CAAC,CAAC,qCAAqC,EAAE,EAAE,EAAE,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC;mBAC9G;YACT,CAAC,CAAC,EAAE;UACJ,IAAI,CAAC,MAAM;YACX,CAAC,CAAC,IAAI,CAAA;gBACA,OAAO,CAAC,CAAC,CAAC,mCAAmC,EAAE;gBAC/C,EAAE,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,kBAAkB,EAAE;gBACxD,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,EAAE;aACzC,CAAC;mBACG;YACT,CAAC,CAAC,EAAE;;kBAEI,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC;;;;mBAIvC,IAAI,CAAC,MAAM;mBACX,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;UAEnD,IAAI,CAAC,QAAQ,KAAK,IAAI;YACtB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,IAAI,CAAA;gBACA,IAAI,CAAC,QAAQ,GAAG,CAAC;gBACjB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC9E,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,oCAAoC,CAAC;mBAC9C;;;;+CAI4B,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;qDAC5C,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,WAAW,IAAI,CAAC,IAAI;YACtI,OAAO,CAAC,CAAC,CAAC,4BAA4B,CAAC;;;KAG9C,CAAA;IACH,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACnB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;gBACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;;SAOZ;gBACD,SAAS,EAAE;oBACT,OAAO,EAAE;wBACP,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,SAAS,EAAE,IAAI,CAAC,SAAS;wBACzB,aAAa,EAAE,IAAI,CAAC,aAAa;wBACjC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;qBAC3B;iBACF;gBACD,OAAO,EAAE,UAAU,EAAE;aACtB,CAAC,CAAA;YAEF,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;gBAC7B,MAAM,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;gBAC7D,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAAC,QAAQ,EAAE;oBACxB,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,sCAAsC,CAAC,CAAC,CAAC,0BAA0B,CAAC,EAAE;iBAC5H,CAAC,CACH,CAAA;gBACD,OAAO,CAAC,IAAI,EAAE,CAAA;YAChB,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,GAAG,KAAK,CAAA;QACtB,CAAC;IACH,CAAC;;AA1H2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;oDAAsB;AACrB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;qDAAuB;AACtB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;yDAAuB;AAGjC;IAAhB,KAAK,EAAE;;mDAA4B;AAEnB;IAAhB,KAAK,EAAE;;kDAA2B;AAElB;IAAhB,KAAK,EAAE;;oDAAuC;AAC9B;IAAhB,KAAK,EAAE;;kDAAoB;AACX;IAAhB,KAAK,EAAE;;mDAAwB;AAnDrB,kBAAkB;IAD9B,aAAa,CAAC,sBAAsB,CAAC;GACzB,kBAAkB,CAmK9B","sourcesContent":["import '@operato/popup/ox-popup-actions.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/textfield/outlined-text-field.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { CommonPopupStyles } from '@operato/styles'\n\nimport { maySend, privilegeWords, recentDenial } from '../utils/access-request.js'\n\n/**\n * Asking for a privilege the person was just refused (ADR-0103, docs/design/access-request.md §3-2).\n *\n * What was needed comes from the refusal - the person writes only why, and must (questions Q9). Before sending\n * the popup says the request is recorded and how many people it reaches; a denial of the last days is shown with\n * its reason, and the same words are not sent again. A request already waiting is shown instead of a blank form:\n * pressing again adds to it, the people deciding are not told twice.\n */\n@customElement('access-request-popup')\nexport class AccessRequestPopup extends localize(i18next)(LitElement) {\n static styles = [\n CommonPopupStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--md-sys-color-surface);\n }\n /* min-height: 0 lets the body scroll inside the popup, so the send button below it stays in view. */\n [body] {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium);\n padding: var(--spacing-large);\n }\n [needed] {\n font: var(--title-font, inherit);\n color: var(--md-sys-color-on-surface);\n }\n [operation],\n [waiting],\n [recorded] {\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface-variant);\n }\n [denied] {\n font: var(--label-font, inherit);\n color: var(--md-sys-color-error);\n }\n md-outlined-text-field {\n width: 100%;\n }\n `\n ]\n\n @property({ type: String }) category: string = ''\n @property({ type: String }) privilege: string = ''\n @property({ type: String }) operationName?: string\n\n /** The request of this person and privilege that is still waiting, if any. */\n @state() private waiting: any = null\n /** A denial of the same privilege in the last days - its reason is shown before the person writes (Q9 · 5). */\n @state() private denied: any = null\n /** How many people a request reaches - said before sending (Q9 · 2). */\n @state() private deciders: number | null = null\n @state() private reason = ''\n @state() private sending = false\n\n async firstUpdated() {\n const response = await client.query({\n query: gql`\n query {\n myAccessRequests {\n id\n category\n privilege\n state\n reason\n count\n lastRequestedAt\n decidedAt\n decisionReason\n }\n accessRequestDeciderCount\n }\n `,\n context: gqlContext(),\n fetchPolicy: 'network-only'\n })\n const mine = response.data?.myAccessRequests || []\n this.waiting = mine.find((r: any) => r.state === 'PENDING' && r.category === this.category && r.privilege === this.privilege) || null\n this.denied = this.waiting ? null : recentDenial(mine, this.category, this.privilege) || null\n this.deciders = typeof response.data?.accessRequestDeciderCount === 'number' ? response.data.accessRequestDeciderCount : null\n this.reason = this.waiting?.reason || ''\n }\n\n render() {\n return html`\n <div body>\n <div needed>${privilegeWords(i18next.t.bind(i18next), this.category, this.privilege)}</div>\n ${this.operationName\n ? html`<div operation>${i18next.t('access-request.text.refused at', { operation: this.operationName })}</div>`\n : ''}\n ${this.waiting\n ? html`<div waiting>\n ${i18next.t('access-request.text.already waiting', { at: new Date(this.waiting.lastRequestedAt).toLocaleString() })}\n </div>`\n : ''}\n ${this.denied\n ? html`<div denied>\n ${i18next.t('access-request.text.denied lately', {\n at: new Date(this.denied.decidedAt).toLocaleDateString(),\n reason: this.denied.decisionReason || ''\n })}\n </div>`\n : ''}\n <md-outlined-text-field\n label=${i18next.t('access-request.field.reason')}\n required\n type=\"textarea\"\n rows=\"2\"\n .value=${this.reason}\n @input=${(e: any) => (this.reason = e.target.value)}\n ></md-outlined-text-field>\n ${this.deciders === null\n ? ''\n : html`<div recorded>\n ${this.deciders > 0\n ? i18next.t('access-request.text.recorded and told', { count: this.deciders })\n : i18next.t('access-request.text.nobody to tell')}\n </div>`}\n </div>\n\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => history.back()}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${this.sending || !maySend(this.reason, this.denied) || this.deciders === 0} @click=${this.send}>\n ${i18next.t('access-request.button.send')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n private async send() {\n this.sending = true\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($request: NewAccessRequest!) {\n requestAccess(request: $request) {\n id\n count\n }\n }\n `,\n variables: {\n request: {\n category: this.category,\n privilege: this.privilege,\n operationName: this.operationName,\n reason: this.reason.trim()\n }\n },\n context: gqlContext()\n })\n\n if (!response.errors?.length) {\n const merged = (response.data?.requestAccess?.count || 1) > 1\n document.dispatchEvent(\n new CustomEvent('notify', {\n detail: { level: 'info', message: i18next.t(merged ? 'access-request.text.added to waiting' : 'access-request.text.sent') }\n })\n )\n history.back()\n }\n } finally {\n this.sending = false\n }\n }\n}\n"]}
1
+ {"version":3,"file":"access-request-popup.js","sourceRoot":"","sources":["../../client/components/access-request-popup.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/popup/ox-popup-actions.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/textfield/outlined-text-field.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { CommonPopupStyles } from '@operato/styles'\n\nimport '@material/web/radio/radio.js'\n\nimport { maySend, privilegeWords, recentDenial, taskName, taskSays } from '../utils/access-request.js'\n\n/**\n * Asking for a privilege the person was just refused (ADR-0103, docs/design/access-request.md §3-2).\n *\n * What was needed comes from the refusal - the person writes only why, and must (questions Q9). It is said as the work\n * it belongs to: every task holding the refused privilege is listed, fewest privileges first, the first picked, and the\n * person picks the one they were doing (ADR-0103 decision 9). A privilege no task holds is said as itself. Before sending\n * the popup says the request is recorded and how many people it reaches; a denial of the last days is shown with\n * its reason, and the same words are not sent again. A request already waiting is shown instead of a blank form:\n * pressing again adds to it, the people deciding are not told twice.\n */\n@customElement('access-request-popup')\nexport class AccessRequestPopup extends localize(i18next)(LitElement) {\n static styles = [\n CommonPopupStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--md-sys-color-surface);\n }\n /* min-height: 0 lets the body scroll inside the popup, so the send button below it stays in view. */\n [body] {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium);\n padding: var(--spacing-large);\n }\n [needed] {\n font: var(--title-font, inherit);\n color: var(--md-sys-color-on-surface);\n }\n label[task] {\n display: flex;\n align-items: flex-start;\n gap: var(--spacing-small);\n }\n [says] {\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface-variant);\n }\n [operation],\n [waiting],\n [recorded] {\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface-variant);\n }\n [denied] {\n font: var(--label-font, inherit);\n color: var(--md-sys-color-error);\n }\n md-outlined-text-field {\n width: 100%;\n }\n `\n ]\n\n @property({ type: String }) category: string = ''\n @property({ type: String }) privilege: string = ''\n @property({ type: String }) operationName?: string\n\n /** The request of this person and privilege that is still waiting, if any. */\n @state() private waiting: any = null\n /** A denial of the same privilege in the last days - its reason is shown before the person writes (Q9 · 5). */\n @state() private denied: any = null\n /** How many people a request reaches - said before sending (Q9 · 2). */\n @state() private deciders: number | null = null\n @state() private reason = ''\n @state() private sending = false\n /** The tasks that hold the refused privilege, fewest privileges first - the server's order (tasksGranting). */\n @state() private tasks: { taskId: string }[] = []\n @state() private taskId?: string\n\n async firstUpdated() {\n const response = await client.query({\n query: gql`\n query ($category: String!, $privilege: String!) {\n myAccessRequests {\n id\n category\n privilege\n taskId\n state\n reason\n count\n lastRequestedAt\n decidedAt\n decisionReason\n }\n accessRequestDeciderCount\n tasksGranting(category: $category, privilege: $privilege) {\n taskId\n }\n }\n `,\n variables: { category: this.category, privilege: this.privilege },\n context: gqlContext(),\n fetchPolicy: 'network-only'\n })\n const mine = response.data?.myAccessRequests || []\n this.waiting = mine.find((r: any) => r.state === 'PENDING' && r.category === this.category && r.privilege === this.privilege) || null\n this.denied = this.waiting ? null : recentDenial(mine, this.category, this.privilege) || null\n this.deciders = typeof response.data?.accessRequestDeciderCount === 'number' ? response.data.accessRequestDeciderCount : null\n this.reason = this.waiting?.reason || ''\n this.tasks = response.data?.tasksGranting || []\n /* A request already waiting keeps the work it named - asking again adds to it and does not change it. */\n this.taskId = this.tasks.find(task => task.taskId === this.waiting?.taskId)?.taskId ?? this.tasks[0]?.taskId\n }\n\n render() {\n return html`\n <div body>\n ${this.renderNeeded()}\n ${this.operationName\n ? html`<div operation>${i18next.t('access-request.text.refused at', { operation: this.operationName })}</div>`\n : ''}\n ${this.waiting\n ? html`<div waiting>\n ${i18next.t('access-request.text.already waiting', { at: new Date(this.waiting.lastRequestedAt).toLocaleString() })}\n </div>`\n : ''}\n ${this.denied\n ? html`<div denied>\n ${i18next.t('access-request.text.denied lately', {\n at: new Date(this.denied.decidedAt).toLocaleDateString(),\n reason: this.denied.decisionReason || ''\n })}\n </div>`\n : ''}\n <md-outlined-text-field\n label=${i18next.t('access-request.field.reason')}\n required\n type=\"textarea\"\n rows=\"2\"\n .value=${this.reason}\n @input=${(e: any) => (this.reason = e.target.value)}\n ></md-outlined-text-field>\n ${this.deciders === null\n ? ''\n : html`<div recorded>\n ${this.deciders > 0\n ? i18next.t('access-request.text.recorded and told', { count: this.deciders })\n : i18next.t('access-request.text.nobody to tell')}\n </div>`}\n </div>\n\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => history.back()}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${this.sending || !maySend(this.reason, this.denied) || this.deciders === 0} @click=${this.send}>\n ${i18next.t('access-request.button.send')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n /** The work the refused privilege belongs to - one line for one task, a choice for several, the privilege for none. */\n private renderNeeded() {\n const t = i18next.t.bind(i18next)\n if (this.tasks.length === 0) return html`<div needed>${privilegeWords(t, this.category, this.privilege)}</div>`\n if (this.tasks.length === 1) {\n const says = taskSays(t, this.tasks[0].taskId)\n return html`<div needed>${taskName(t, this.tasks[0].taskId)}</div>\n ${says ? html`<div says>${says}</div>` : ''}`\n }\n return this.tasks.map(\n task => html`\n <label task>\n <md-radio name=\"task\" .checked=${task.taskId === this.taskId} @change=${() => (this.taskId = task.taskId)}></md-radio>\n <span>\n <div needed>${taskName(t, task.taskId)}</div>\n ${taskSays(t, task.taskId) ? html`<div says>${taskSays(t, task.taskId)}</div>` : ''}\n </span>\n </label>\n `\n )\n }\n\n private async send() {\n this.sending = true\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($request: NewAccessRequest!) {\n requestAccess(request: $request) {\n id\n count\n }\n }\n `,\n variables: {\n request: {\n category: this.category,\n privilege: this.privilege,\n operationName: this.operationName,\n taskId: this.taskId,\n reason: this.reason.trim()\n }\n },\n context: gqlContext()\n })\n\n if (!response.errors?.length) {\n const merged = (response.data?.requestAccess?.count || 1) > 1\n document.dispatchEvent(\n new CustomEvent('notify', {\n detail: { level: 'info', message: i18next.t(merged ? 'access-request.text.added to waiting' : 'access-request.text.sent') }\n })\n )\n history.back()\n }\n } finally {\n this.sending = false\n }\n }\n}\n"]}
@@ -0,0 +1,22 @@
1
+ export type TaskGrant = {
2
+ category: string;
3
+ axes: string[];
4
+ };
5
+ export type TaskCoverageRef = {
6
+ taskId: string;
7
+ status: 'all' | 'some' | 'none' | 'broken';
8
+ held: TaskGrant[];
9
+ missing: TaskGrant[];
10
+ };
11
+ /** `category:axis` — one privilege. */
12
+ export declare const pairOf: (category: string, axis: string) => string;
13
+ /** Adding a task adds what the role lacks of it. A task naming an undeclared privilege is not added. */
14
+ export declare function pairsToAdd(task: TaskCoverageRef): string[];
15
+ /**
16
+ * Taking a task off takes off its privileges **except those another task the role holds whole still uses** — so
17
+ * taking one task off never leaves another 「일부」. Those are kept and named.
18
+ */
19
+ export declare function pairsToRemove(task: TaskCoverageRef, coverage: TaskCoverageRef[]): {
20
+ remove: string[];
21
+ kept: string[];
22
+ };
@@ -0,0 +1,17 @@
1
+ /** `category:axis` — one privilege. */
2
+ export const pairOf = (category, axis) => `${category}:${axis}`;
3
+ const pairsOf = (grants) => grants.flatMap(grant => grant.axes.map(axis => pairOf(grant.category, axis)));
4
+ /** Adding a task adds what the role lacks of it. A task naming an undeclared privilege is not added. */
5
+ export function pairsToAdd(task) {
6
+ return task.status === 'broken' ? [] : pairsOf(task.missing);
7
+ }
8
+ /**
9
+ * Taking a task off takes off its privileges **except those another task the role holds whole still uses** — so
10
+ * taking one task off never leaves another 「일부」. Those are kept and named.
11
+ */
12
+ export function pairsToRemove(task, coverage) {
13
+ const needed = new Set(coverage.filter(other => other.taskId !== task.taskId && other.status === 'all').flatMap(other => pairsOf(other.held)));
14
+ const held = pairsOf(task.held);
15
+ return { remove: held.filter(pair => !needed.has(pair)), kept: held.filter(pair => needed.has(pair)) };
16
+ }
17
+ //# sourceMappingURL=role-task-edit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-task-edit.js","sourceRoot":"","sources":["../../client/components/role-task-edit.ts"],"names":[],"mappings":"AAWA,uCAAuC;AACvC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,QAAgB,EAAE,IAAY,EAAE,EAAE,CAAC,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAA;AAC/E,MAAM,OAAO,GAAG,CAAC,MAAmB,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;AAEtH,wGAAwG;AACxG,MAAM,UAAU,UAAU,CAAC,IAAqB;IAC9C,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;AAC9D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAqB,EAAE,QAA2B;IAC9E,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC9I,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC/B,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,CAAA;AACxG,CAAC","sourcesContent":["/*\n * Adding a task to a role and taking one off (ADR-0139 decision 11) — the rule, apart from the page so it is tested\n * without a DOM.\n *\n * Nothing here knows a task by itself: every list comes from the server's `Role.taskCoverage` (a task's held and\n * missing privileges are its whole list in this installation). The role stores privileges, never tasks, so an edit\n * is a set of privileges to add or to take off.\n */\nexport type TaskGrant = { category: string; axes: string[] }\nexport type TaskCoverageRef = { taskId: string; status: 'all' | 'some' | 'none' | 'broken'; held: TaskGrant[]; missing: TaskGrant[] }\n\n/** `category:axis` — one privilege. */\nexport const pairOf = (category: string, axis: string) => `${category}:${axis}`\nconst pairsOf = (grants: TaskGrant[]) => grants.flatMap(grant => grant.axes.map(axis => pairOf(grant.category, axis)))\n\n/** Adding a task adds what the role lacks of it. A task naming an undeclared privilege is not added. */\nexport function pairsToAdd(task: TaskCoverageRef): string[] {\n return task.status === 'broken' ? [] : pairsOf(task.missing)\n}\n\n/**\n * Taking a task off takes off its privileges **except those another task the role holds whole still uses** — so\n * taking one task off never leaves another 「일부」. Those are kept and named.\n */\nexport function pairsToRemove(task: TaskCoverageRef, coverage: TaskCoverageRef[]): { remove: string[]; kept: string[] } {\n const needed = new Set(coverage.filter(other => other.taskId !== task.taskId && other.status === 'all').flatMap(other => pairsOf(other.held)))\n const held = pairsOf(task.held)\n return { remove: held.filter(pair => !needed.has(pair)), kept: held.filter(pair => needed.has(pair)) }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { pairsToAdd, pairsToRemove } from './role-task-edit';
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
+ describe('adding a task to a role', () => {
7
+ it('★ adds what the role lacks of it, nothing it already holds', () => {
8
+ expect(pairsToAdd(task('organization.people-keeping', 'some', ['employee:query'], ['employee:mutation']))).toEqual(['employee:mutation']);
9
+ });
10
+ it('adds nothing for a task that names an undeclared privilege', () => {
11
+ expect(pairsToAdd(task('board.exporting', 'broken', [], ['board:query']))).toEqual([]);
12
+ });
13
+ });
14
+ describe('taking a task off a role', () => {
15
+ const reading = task('organization.people-reading', 'all', ['employee:query', 'department:query']);
16
+ const keeping = task('organization.people-keeping', 'all', ['employee:query', 'employee:mutation', 'department:query', 'department:mutation']);
17
+ it('★ keeps what another task the role holds whole still uses, and names it', () => {
18
+ expect(pairsToRemove(keeping, [reading, keeping])).toEqual({
19
+ remove: ['employee:mutation', 'department:mutation'],
20
+ kept: ['employee:query', 'department:query']
21
+ });
22
+ });
23
+ it('★ takes everything off when no other whole task uses it', () => {
24
+ expect(pairsToRemove(keeping, [keeping])).toEqual({ remove: ['employee:query', 'employee:mutation', 'department:query', 'department:mutation'], kept: [] });
25
+ });
26
+ it('does not keep a privilege for a task the role holds only in part', () => {
27
+ const partly = task('organization.people-keeping', 'some', ['employee:query'], ['employee:mutation']);
28
+ expect(pairsToRemove(reading, [reading, partly])).toEqual({ remove: ['employee:query', 'department:query'], kept: [] });
29
+ });
30
+ it('may leave nothing to take off - every privilege is another whole task’s', () => {
31
+ expect(pairsToRemove(reading, [reading, keeping])).toEqual({ remove: [], kept: ['employee:query', 'department:query'] });
32
+ });
33
+ });
34
+ //# sourceMappingURL=role-task-edit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-task-edit.test.js","sourceRoot":"","sources":["../../client/components/role-task-edit.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { pairsToAdd, pairsToRemove, 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}\n\ndescribe('adding a task to a role', () => {\n it('★ adds what the role lacks of it, nothing it already holds', () => {\n expect(pairsToAdd(task('organization.people-keeping', 'some', ['employee:query'], ['employee:mutation']))).toEqual(['employee:mutation'])\n })\n\n it('adds nothing for a task that names an undeclared privilege', () => {\n expect(pairsToAdd(task('board.exporting', 'broken', [], ['board:query']))).toEqual([])\n })\n})\n\ndescribe('taking a task off a role', () => {\n const reading = task('organization.people-reading', 'all', ['employee:query', 'department:query'])\n const keeping = task('organization.people-keeping', 'all', ['employee:query', 'employee:mutation', 'department:query', 'department:mutation'])\n\n it('★ keeps what another task the role holds whole still uses, and names it', () => {\n expect(pairsToRemove(keeping, [reading, keeping])).toEqual({\n remove: ['employee:mutation', 'department:mutation'],\n kept: ['employee:query', 'department:query']\n })\n })\n\n it('★ takes everything off when no other whole task uses it', () => {\n expect(pairsToRemove(keeping, [keeping])).toEqual({ remove: ['employee:query', 'employee:mutation', 'department:query', 'department:mutation'], kept: [] })\n })\n\n it('does not keep a privilege for a task the role holds only in part', () => {\n const partly = task('organization.people-keeping', 'some', ['employee:query'], ['employee:mutation'])\n expect(pairsToRemove(reading, [reading, partly])).toEqual({ remove: ['employee:query', 'department:query'], kept: [] })\n })\n\n it('may leave nothing to take off - every privilege is another whole task’s', () => {\n expect(pairsToRemove(reading, [reading, keeping])).toEqual({ remove: [], kept: ['employee:query', 'department:query'] })\n })\n})\n"]}
@@ -12,7 +12,7 @@ import { navigate, PageView } from '@operato/shell';
12
12
  import { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
13
13
  import { isMobileDevice } from '@operato/utils';
14
14
  import { p13n } from '@operato/p13n';
15
- import { privilegeWords } from '../../utils/access-request.js';
15
+ import { neededWords } from '../../utils/access-request.js';
16
16
  import { listOutcome, pageFromResponse } from '../../utils/list-result.js';
17
17
  /**
18
18
  * Who asked for what in this domain, and what became of it (ADR-0103).
@@ -118,6 +118,7 @@ let AccessRequestsPage = class AccessRequestsPage extends p13n(localize(i18next)
118
118
  id
119
119
  category
120
120
  privilege
121
+ taskId
121
122
  reason
122
123
  state
123
124
  requester {
@@ -146,6 +147,7 @@ let AccessRequestsPage = class AccessRequestsPage extends p13n(localize(i18next)
146
147
  id
147
148
  category
148
149
  privilege
150
+ taskId
149
151
  operationName
150
152
  reason
151
153
  count
@@ -186,7 +188,7 @@ let AccessRequestsPage = class AccessRequestsPage extends p13n(localize(i18next)
186
188
  /* A pending row past its time is expired - the same reading the review makes. */
187
189
  state: item.state === 'PENDING' && item.expiresAt && new Date(item.expiresAt).getTime() <= Date.now() ? 'EXPIRED' : item.state,
188
190
  requesterName: item.requester?.name || item.requester?.email || '',
189
- needed: privilegeWords(t, item.category, item.privilege),
191
+ needed: neededWords(t, item),
190
192
  deciderName: item.decidedBy?.name || '',
191
193
  grantedRoleName: item.grantedRole?.name || ''
192
194
  })).map((record) => ({ ...record, stateWords: t(`access-request.state.${record.state.toLowerCase()}`) }))
@@ -1 +1 @@
1
- {"version":3,"file":"access-requests.js","sourceRoot":"","sources":["../../../client/pages/access-request/access-requests.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/data-grist/ox-grist.js'\nimport '../../components/access-decision-popup.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { p13n } from '@operato/p13n'\n\nimport { privilegeWords } from '../../utils/access-request.js'\nimport { listOutcome, pageFromResponse } from '../../utils/list-result.js'\n\n/**\n * Who asked for what in this domain, and what became of it (ADR-0103).\n *\n * This list is where the people deciding receive requests - in every product, worklist or not. A\n * notification may point here; when none went out the request is here all the same. The rows are the\n * record and are never deleted: who decided, which role, why not.\n */\n@customElement('access-requests-page')\nexport class AccessRequestsPage extends p13n(localize(i18next)(PageView)) {\n static styles = [\n WorkSurfacePageStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n [failed] {\n padding: var(--spacing-medium);\n color: var(--md-sys-color-error);\n font: var(--label-font, inherit);\n }\n `\n ]\n\n @property({ type: Boolean }) active: boolean = false\n @property({ type: Object }) config: any\n @property({ type: String }) mode: 'CARD' | 'LIST' | 'GRID' = isMobileDevice() ? 'CARD' : 'GRID'\n @property({ type: Boolean }) failed: boolean = false\n\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n return {\n title: i18next.t('access-request.title.access requests'),\n help: 'auth/user',\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n actions: [{ title: i18next.t('text.users'), action: () => navigate('users'), icon: 'group' }]\n }\n }\n\n render() {\n return html`\n <ox-grist\n class=\"page-fill\"\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n ></ox-grist>\n ${this.failed ? html`<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}\n `\n }\n\n async pageInitialized() {\n const t = i18next.t.bind(i18next)\n this.config = {\n rows: { appendable: false, selectable: { multiple: false } },\n sorters: [{ name: 'createdAt', desc: true }],\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n /* Deciding means something only while it waits - offered only there. */\n {\n type: 'gutter',\n gutterName: 'button',\n icon: 'gavel',\n title: t('access-request.button.decide'),\n handlers: { click: (_c: any, _d: any, _col: any, record: any) => record?.state === 'PENDING' && this.decide(record) }\n },\n { type: 'string', name: 'requesterName', header: t('access-request.field.requester'), record: { editable: false }, width: 140 },\n { type: 'string', name: 'needed', header: t('access-request.field.needed'), record: { editable: false }, width: 260 },\n { type: 'string', name: 'reason', header: t('access-request.field.reason'), record: { editable: false }, filter: 'search', width: 200 },\n { type: 'string', name: 'stateWords', header: t('field.state'), record: { align: 'center', editable: false }, width: 100 },\n { type: 'integer', name: 'count', header: t('access-request.field.count'), record: { align: 'right', editable: false }, width: 70 },\n { type: 'datetime', name: 'createdAt', header: t('field.created at'), sortable: true, width: 170 },\n { type: 'string', name: 'deciderName', header: t('access-request.field.decided by'), record: { editable: false }, width: 140 },\n { type: 'string', name: 'grantedRoleName', header: t('access-request.field.granted role'), record: { editable: false }, width: 140 },\n { type: 'string', name: 'decisionReason', header: t('access-request.field.decision reason'), record: { editable: false }, width: 200 },\n { type: 'datetime', name: 'decidedAt', header: t('access-request.field.decided at'), sortable: true, width: 170 }\n ]\n }\n }\n\n private decide(record: any) {\n openPopup(\n html`<access-decision-popup .request=${record} @access-request-decided=${() => this.grist.fetch()}></access-decision-popup>`,\n { size: 'medium', title: i18next.t('access-request.title.decide') }\n )\n }\n\n async pageUpdated(_changes: any, lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n /* A notification links here with the request's id - its decision opens at once, if it still waits. */\n const id = lifecycle?.params?.id\n if (id && id !== this.opened) {\n this.opened = id\n await this.openById(id)\n }\n }\n }\n\n /** The request a link named, opened only while it waits - a decided one is just a row in the list. */\n private opened?: string\n\n private async openById(id: string) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!]) {\n accessRequests(filters: $filters) {\n items {\n id\n category\n privilege\n reason\n state\n requester {\n id\n name\n email\n }\n }\n }\n }\n `,\n variables: { filters: [{ name: 'id', operator: 'eq', value: id }] },\n context: gqlContext(),\n fetchPolicy: 'network-only'\n })\n const request = pageFromResponse(response, 'accessRequests')?.items?.[0]\n if (request?.state === 'PENDING') this.decide(request)\n }\n\n async fetchHandler({ page, limit, sortings = [], filters = [] }: FetchOption) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n accessRequests(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n category\n privilege\n operationName\n reason\n count\n state\n expiresAt\n createdAt\n decidedAt\n decisionReason\n requester {\n id\n name\n email\n }\n decidedBy {\n id\n name\n }\n grantedRole {\n id\n name\n }\n }\n total\n }\n }\n `,\n variables: { filters, pagination: { page, limit }, sortings },\n context: gqlContext(),\n fetchPolicy: 'network-only'\n })\n\n const { items, total, failed } = listOutcome(pageFromResponse(response, 'accessRequests'))\n this.failed = failed\n const t = i18next.t.bind(i18next)\n\n return {\n total,\n records: items.map((item: any) => ({\n ...item,\n /* A pending row past its time is expired - the same reading the review makes. */\n state: item.state === 'PENDING' && item.expiresAt && new Date(item.expiresAt).getTime() <= Date.now() ? 'EXPIRED' : item.state,\n requesterName: item.requester?.name || item.requester?.email || '',\n needed: privilegeWords(t, item.category, item.privilege),\n deciderName: item.decidedBy?.name || '',\n grantedRoleName: item.grantedRole?.name || ''\n })).map((record: any) => ({ ...record, stateWords: t(`access-request.state.${record.state.toLowerCase()}`) }))\n }\n }\n}\n"]}
1
+ {"version":3,"file":"access-requests.js","sourceRoot":"","sources":["../../../client/pages/access-request/access-requests.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/data-grist/ox-grist.js'\nimport '../../components/access-decision-popup.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { p13n } from '@operato/p13n'\n\nimport { neededWords } from '../../utils/access-request.js'\nimport { listOutcome, pageFromResponse } from '../../utils/list-result.js'\n\n/**\n * Who asked for what in this domain, and what became of it (ADR-0103).\n *\n * This list is where the people deciding receive requests - in every product, worklist or not. A\n * notification may point here; when none went out the request is here all the same. The rows are the\n * record and are never deleted: who decided, which role, why not.\n */\n@customElement('access-requests-page')\nexport class AccessRequestsPage extends p13n(localize(i18next)(PageView)) {\n static styles = [\n WorkSurfacePageStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n [failed] {\n padding: var(--spacing-medium);\n color: var(--md-sys-color-error);\n font: var(--label-font, inherit);\n }\n `\n ]\n\n @property({ type: Boolean }) active: boolean = false\n @property({ type: Object }) config: any\n @property({ type: String }) mode: 'CARD' | 'LIST' | 'GRID' = isMobileDevice() ? 'CARD' : 'GRID'\n @property({ type: Boolean }) failed: boolean = false\n\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n return {\n title: i18next.t('access-request.title.access requests'),\n help: 'auth/user',\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n actions: [{ title: i18next.t('text.users'), action: () => navigate('users'), icon: 'group' }]\n }\n }\n\n render() {\n return html`\n <ox-grist\n class=\"page-fill\"\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n ></ox-grist>\n ${this.failed ? html`<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}\n `\n }\n\n async pageInitialized() {\n const t = i18next.t.bind(i18next)\n this.config = {\n rows: { appendable: false, selectable: { multiple: false } },\n sorters: [{ name: 'createdAt', desc: true }],\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n /* Deciding means something only while it waits - offered only there. */\n {\n type: 'gutter',\n gutterName: 'button',\n icon: 'gavel',\n title: t('access-request.button.decide'),\n handlers: { click: (_c: any, _d: any, _col: any, record: any) => record?.state === 'PENDING' && this.decide(record) }\n },\n { type: 'string', name: 'requesterName', header: t('access-request.field.requester'), record: { editable: false }, width: 140 },\n { type: 'string', name: 'needed', header: t('access-request.field.needed'), record: { editable: false }, width: 260 },\n { type: 'string', name: 'reason', header: t('access-request.field.reason'), record: { editable: false }, filter: 'search', width: 200 },\n { type: 'string', name: 'stateWords', header: t('field.state'), record: { align: 'center', editable: false }, width: 100 },\n { type: 'integer', name: 'count', header: t('access-request.field.count'), record: { align: 'right', editable: false }, width: 70 },\n { type: 'datetime', name: 'createdAt', header: t('field.created at'), sortable: true, width: 170 },\n { type: 'string', name: 'deciderName', header: t('access-request.field.decided by'), record: { editable: false }, width: 140 },\n { type: 'string', name: 'grantedRoleName', header: t('access-request.field.granted role'), record: { editable: false }, width: 140 },\n { type: 'string', name: 'decisionReason', header: t('access-request.field.decision reason'), record: { editable: false }, width: 200 },\n { type: 'datetime', name: 'decidedAt', header: t('access-request.field.decided at'), sortable: true, width: 170 }\n ]\n }\n }\n\n private decide(record: any) {\n openPopup(\n html`<access-decision-popup .request=${record} @access-request-decided=${() => this.grist.fetch()}></access-decision-popup>`,\n { size: 'medium', title: i18next.t('access-request.title.decide') }\n )\n }\n\n async pageUpdated(_changes: any, lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n /* A notification links here with the request's id - its decision opens at once, if it still waits. */\n const id = lifecycle?.params?.id\n if (id && id !== this.opened) {\n this.opened = id\n await this.openById(id)\n }\n }\n }\n\n /** The request a link named, opened only while it waits - a decided one is just a row in the list. */\n private opened?: string\n\n private async openById(id: string) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!]) {\n accessRequests(filters: $filters) {\n items {\n id\n category\n privilege\n taskId\n reason\n state\n requester {\n id\n name\n email\n }\n }\n }\n }\n `,\n variables: { filters: [{ name: 'id', operator: 'eq', value: id }] },\n context: gqlContext(),\n fetchPolicy: 'network-only'\n })\n const request = pageFromResponse(response, 'accessRequests')?.items?.[0]\n if (request?.state === 'PENDING') this.decide(request)\n }\n\n async fetchHandler({ page, limit, sortings = [], filters = [] }: FetchOption) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n accessRequests(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n category\n privilege\n taskId\n operationName\n reason\n count\n state\n expiresAt\n createdAt\n decidedAt\n decisionReason\n requester {\n id\n name\n email\n }\n decidedBy {\n id\n name\n }\n grantedRole {\n id\n name\n }\n }\n total\n }\n }\n `,\n variables: { filters, pagination: { page, limit }, sortings },\n context: gqlContext(),\n fetchPolicy: 'network-only'\n })\n\n const { items, total, failed } = listOutcome(pageFromResponse(response, 'accessRequests'))\n this.failed = failed\n const t = i18next.t.bind(i18next)\n\n return {\n total,\n records: items.map((item: any) => ({\n ...item,\n /* A pending row past its time is expired - the same reading the review makes. */\n state: item.state === 'PENDING' && item.expiresAt && new Date(item.expiresAt).getTime() <= Date.now() ? 'EXPIRED' : item.state,\n requesterName: item.requester?.name || item.requester?.email || '',\n needed: neededWords(t, item),\n deciderName: item.decidedBy?.name || '',\n grantedRoleName: item.grantedRole?.name || ''\n })).map((record: any) => ({ ...record, stateWords: t(`access-request.state.${record.state.toLowerCase()}`) }))\n }\n }\n}\n"]}
@@ -9,7 +9,7 @@ import { OxPrompt } from '@operato/popup/ox-prompt.js';
9
9
  import { PageView } from '@operato/shell';
10
10
  import { CommonGristStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
11
11
  import { isMobileDevice } from '@operato/utils';
12
- import { privilegeWords } from '../../utils/access-request.js';
12
+ import { neededWords } from '../../utils/access-request.js';
13
13
  import { arrayFromResponse, listOutcome } from '../../utils/list-result.js';
14
14
  /**
15
15
  * The person's own access requests - what became of each (ADR-0103). A notification may not reach them;
@@ -99,6 +99,7 @@ let MyAccessRequestsPage = class MyAccessRequestsPage extends localize(i18next)(
99
99
  id
100
100
  category
101
101
  privilege
102
+ taskId
102
103
  state
103
104
  expiresAt
104
105
  createdAt
@@ -121,7 +122,7 @@ let MyAccessRequestsPage = class MyAccessRequestsPage extends localize(i18next)(
121
122
  ...item,
122
123
  state,
123
124
  stateWords: t(`access-request.state.${state.toLowerCase()}`),
124
- needed: privilegeWords(t, item.category, item.privilege),
125
+ needed: neededWords(t, item),
125
126
  grantedRoleName: item.grantedRole?.name || ''
126
127
  };
127
128
  });