@things-factory/auth-ui 10.1.194 → 10.1.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist-client/bootstrap.js +4 -1
  2. package/dist-client/bootstrap.js.map +1 -1
  3. package/dist-client/components/access-decision-popup.js +7 -5
  4. package/dist-client/components/access-decision-popup.js.map +1 -1
  5. package/dist-client/components/access-request-popup.d.ts +9 -1
  6. package/dist-client/components/access-request-popup.js +55 -4
  7. package/dist-client/components/access-request-popup.js.map +1 -1
  8. package/dist-client/components/my-login-history.js +14 -10
  9. package/dist-client/components/my-login-history.js.map +1 -1
  10. package/dist-client/components/role-task-edit.d.ts +22 -0
  11. package/dist-client/components/role-task-edit.js +17 -0
  12. package/dist-client/components/role-task-edit.js.map +1 -0
  13. package/dist-client/components/role-task-edit.test.d.ts +1 -0
  14. package/dist-client/components/role-task-edit.test.js +34 -0
  15. package/dist-client/components/role-task-edit.test.js.map +1 -0
  16. package/dist-client/components/role-template-picker.d.ts +5 -0
  17. package/dist-client/components/role-template-picker.js +41 -5
  18. package/dist-client/components/role-template-picker.js.map +1 -1
  19. package/dist-client/components/user-status-renderer.d.ts +9 -0
  20. package/dist-client/components/user-status-renderer.js +36 -0
  21. package/dist-client/components/user-status-renderer.js.map +1 -0
  22. package/dist-client/entity360/person-360.d.ts +13 -0
  23. package/dist-client/entity360/person-360.js +96 -0
  24. package/dist-client/entity360/person-360.js.map +1 -1
  25. package/dist-client/index.d.ts +1 -0
  26. package/dist-client/index.js +14 -1
  27. package/dist-client/index.js.map +1 -1
  28. package/dist-client/pages/access-request/access-requests.js +4 -2
  29. package/dist-client/pages/access-request/access-requests.js.map +1 -1
  30. package/dist-client/pages/access-request/my-access-requests.js +3 -2
  31. package/dist-client/pages/access-request/my-access-requests.js.map +1 -1
  32. package/dist-client/pages/audit/audit-page.d.ts +46 -0
  33. package/dist-client/pages/audit/audit-page.js +417 -0
  34. package/dist-client/pages/audit/audit-page.js.map +1 -0
  35. package/dist-client/pages/role/role-privilege-page.d.ts +46 -0
  36. package/dist-client/pages/role/role-privilege-page.js +500 -5
  37. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  38. package/dist-client/pages/role/role-task-review.test.d.ts +1 -0
  39. package/dist-client/pages/role/role-task-review.test.js +60 -0
  40. package/dist-client/pages/role/role-task-review.test.js.map +1 -0
  41. package/dist-client/pages/user/user-list-page.js +1 -31
  42. package/dist-client/pages/user/user-list-page.js.map +1 -1
  43. package/dist-client/route.js +3 -0
  44. package/dist-client/route.js.map +1 -1
  45. package/dist-client/tsconfig.tsbuildinfo +1 -1
  46. package/dist-client/utils/access-request.d.ts +15 -1
  47. package/dist-client/utils/access-request.js +19 -2
  48. package/dist-client/utils/access-request.js.map +1 -1
  49. package/dist-client/utils/access-request.test.js +26 -1
  50. package/dist-client/utils/access-request.test.js.map +1 -1
  51. package/dist-client/utils/audit-words.d.ts +30 -0
  52. package/dist-client/utils/audit-words.js +57 -0
  53. package/dist-client/utils/audit-words.js.map +1 -0
  54. package/dist-client/utils/audit-words.test.d.ts +1 -0
  55. package/dist-client/utils/audit-words.test.js +47 -0
  56. package/dist-client/utils/audit-words.test.js.map +1 -0
  57. package/dist-server/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +4 -4
  59. package/things-factory.config.js +1 -0
  60. package/translations/en.json +78 -1
  61. package/translations/es.json +78 -1
  62. package/translations/ja.json +78 -1
  63. package/translations/ko.json +78 -1
  64. package/translations/ms.json +78 -1
  65. package/translations/zh.json +78 -1
@@ -8,12 +8,15 @@ import { openPopup, registerNotifyAction, TOOL_POSITION } from '@operato/layout'
8
8
  import { OxGristEditorPrivilege } from '@operato/app/grist-editor/ox-grist-editor-privilege.js';
9
9
  import { registerEditor as registerGristEditor, registerRenderer as registerGristRenderer, OxGristRendererJson5 } from '@operato/data-grist';
10
10
  import { auth } from '@things-factory/auth-base/dist-client/auth.js';
11
+ import { hasPrivilege } from '@things-factory/auth-base/dist-client';
11
12
  import { ADD_MORENDA } from '@things-factory/more-base/client';
12
- import { registerPersonKind } from './entity360/person-360.js';
13
+ import { registerPersonAuditSection, registerPersonKind } from './entity360/person-360.js';
13
14
  import { requestableFrom } from './utils/access-request.js';
14
15
  export default async function bootstrap() {
15
16
  /* the person 360 - a User value anywhere opens it (ADR-0067) */
16
17
  registerPersonKind();
18
+ /* Whose audit a person 360 shows is `audit:query`'s - only those who hold it get the section (ADR-0141 §5). */
19
+ hasPrivilege({ privilege: 'query', category: 'audit', superUserGranted: true }).then(holds => holds && registerPersonAuditSection());
17
20
  registerGristEditor('privilege', OxGristEditorPrivilege);
18
21
  registerGristRenderer('privilege', OxGristRendererJson5);
19
22
  /* add user profile morenda */
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../client/bootstrap.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/i18n/ox-i18n.js'\nimport { i18next } from '@operato/i18n'\nimport '@operato/attribute/grist-editor' /* for register data-grist editor type 'attributes' */\n\nimport { html } from 'lit-html'\n\nimport { navigate, removeDraftsOf, store } from '@operato/shell'\nimport { openPopup, registerNotifyAction, TOOL_POSITION } from '@operato/layout'\nimport { OxGristEditorPrivilege } from '@operato/app/grist-editor/ox-grist-editor-privilege.js'\nimport {\n registerEditor as registerGristEditor,\n registerRenderer as registerGristRenderer,\n OxGristRendererJson5\n} from '@operato/data-grist'\n\nimport { auth } from '@things-factory/auth-base/dist-client/auth.js'\nimport { ADD_MORENDA } from '@things-factory/more-base/client'\nimport { registerPersonKind } from './entity360/person-360.js'\nimport { requestableFrom } from './utils/access-request.js'\n\nexport default async function bootstrap() {\n /* the person 360 - a User value anywhere opens it (ADR-0067) */\n registerPersonKind()\n\n registerGristEditor('privilege', OxGristEditorPrivilege)\n registerGristRenderer('privilege', OxGristRendererJson5)\n\n /* add user profile morenda */\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>account_circle</md-icon> `,\n name: html` <ox-i18n msgid=\"text.auth profile\"></ox-i18n> `,\n position: TOOL_POSITION.FRONT_END,\n action: () => {\n navigate('profile')\n }\n }\n })\n\n /* add sign-out morenda */\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>exit_to_app</md-icon> `,\n name: html` <ox-i18n msgid=\"field.sign out\"></ox-i18n> `,\n position: TOOL_POSITION.FRONT_END,\n action: () => {\n try {\n if (typeof localStorage !== 'undefined') {\n localStorage.removeItem('i18nextLng')\n localStorage.removeItem('i18next')\n }\n\n ;['i18next', 'i18nextLng'].forEach(name => {\n try {\n document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`\n } catch (e) {\n // ignore\n }\n })\n } catch (e) {\n // ignore\n }\n\n auth.signout()\n }\n }\n })\n\n /*\n * Unsaved drafts stay in this browser (ADR-0098). Signing out takes this user's away, so a shared\n * computer keeps none of their edits. Awaited before the page leaves; a browser that keeps no drafts\n * has none to take.\n */\n auth.on('presignout', async () => {\n const user = (auth.credential as any)?.id\n if (user) await removeDraftsOf(user).catch(() => {})\n })\n\n /*\n * A refusal a role would open, of a change the person made, gets one quiet \"request access…\" on its toast. The toast\n * does not know what an error means and @operato/graphql only reports it; this package knows, so it puts the button\n * on (registerNotifyAction, things-factory ADR-0103 decision 5). Where auth-ui is not loaded there is no button.\n */\n registerNotifyAction('access-request', notification => {\n const refusal = Array.isArray(notification.ex) ? notification.ex[0] : undefined\n const detail = requestableFrom(refusal?.extensions, Array.isArray(refusal?.path) ? String(refusal.path[0]) : undefined)\n if (!detail) return null\n return {\n label: i18next.t('access-request.button.request'),\n emphasis: 'quiet',\n callback: () =>\n import('./components/access-request-popup.js').then(() =>\n openPopup(\n html`<access-request-popup\n .category=${detail.category}\n .privilege=${detail.privilege}\n .operationName=${detail.operationName}\n ></access-request-popup>`,\n { size: 'medium', title: i18next.t('access-request.title.request access') }\n )\n )\n }\n })\n\n auth.on('profile', ({ credential }) => {\n if (credential && credential.locale) {\n i18next.changeLanguage(credential.locale)\n }\n })\n\n /* \n Get user profile information from server. \n As soon as response received, auth.on('profile', ...) handlers start to work.\n It's very important point to build UI for the user.\n */\n await auth.profile()\n}\n"]}
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../client/bootstrap.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/i18n/ox-i18n.js'\nimport { i18next } from '@operato/i18n'\nimport '@operato/attribute/grist-editor' /* for register data-grist editor type 'attributes' */\n\nimport { html } from 'lit-html'\n\nimport { navigate, removeDraftsOf, store } from '@operato/shell'\nimport { openPopup, registerNotifyAction, TOOL_POSITION } from '@operato/layout'\nimport { OxGristEditorPrivilege } from '@operato/app/grist-editor/ox-grist-editor-privilege.js'\nimport {\n registerEditor as registerGristEditor,\n registerRenderer as registerGristRenderer,\n OxGristRendererJson5\n} from '@operato/data-grist'\n\nimport { auth } from '@things-factory/auth-base/dist-client/auth.js'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\nimport { ADD_MORENDA } from '@things-factory/more-base/client'\nimport { registerPersonAuditSection, registerPersonKind } from './entity360/person-360.js'\nimport { requestableFrom } from './utils/access-request.js'\n\nexport default async function bootstrap() {\n /* the person 360 - a User value anywhere opens it (ADR-0067) */\n registerPersonKind()\n\n /* Whose audit a person 360 shows is `audit:query`'s - only those who hold it get the section (ADR-0141 §5). */\n hasPrivilege({ privilege: 'query', category: 'audit', superUserGranted: true }).then(holds => holds && registerPersonAuditSection())\n\n registerGristEditor('privilege', OxGristEditorPrivilege)\n registerGristRenderer('privilege', OxGristRendererJson5)\n\n /* add user profile morenda */\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>account_circle</md-icon> `,\n name: html` <ox-i18n msgid=\"text.auth profile\"></ox-i18n> `,\n position: TOOL_POSITION.FRONT_END,\n action: () => {\n navigate('profile')\n }\n }\n })\n\n /* add sign-out morenda */\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>exit_to_app</md-icon> `,\n name: html` <ox-i18n msgid=\"field.sign out\"></ox-i18n> `,\n position: TOOL_POSITION.FRONT_END,\n action: () => {\n try {\n if (typeof localStorage !== 'undefined') {\n localStorage.removeItem('i18nextLng')\n localStorage.removeItem('i18next')\n }\n\n ;['i18next', 'i18nextLng'].forEach(name => {\n try {\n document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`\n } catch (e) {\n // ignore\n }\n })\n } catch (e) {\n // ignore\n }\n\n auth.signout()\n }\n }\n })\n\n /*\n * Unsaved drafts stay in this browser (ADR-0098). Signing out takes this user's away, so a shared\n * computer keeps none of their edits. Awaited before the page leaves; a browser that keeps no drafts\n * has none to take.\n */\n auth.on('presignout', async () => {\n const user = (auth.credential as any)?.id\n if (user) await removeDraftsOf(user).catch(() => {})\n })\n\n /*\n * A refusal a role would open, of a change the person made, gets one quiet \"request access…\" on its toast. The toast\n * does not know what an error means and @operato/graphql only reports it; this package knows, so it puts the button\n * on (registerNotifyAction, things-factory ADR-0103 decision 5). Where auth-ui is not loaded there is no button.\n */\n registerNotifyAction('access-request', notification => {\n const refusal = Array.isArray(notification.ex) ? notification.ex[0] : undefined\n const detail = requestableFrom(refusal?.extensions, Array.isArray(refusal?.path) ? String(refusal.path[0]) : undefined)\n if (!detail) return null\n return {\n label: i18next.t('access-request.button.request'),\n emphasis: 'quiet',\n callback: () =>\n import('./components/access-request-popup.js').then(() =>\n openPopup(\n html`<access-request-popup\n .category=${detail.category}\n .privilege=${detail.privilege}\n .operationName=${detail.operationName}\n ></access-request-popup>`,\n { size: 'medium', title: i18next.t('access-request.title.request access') }\n )\n )\n }\n })\n\n auth.on('profile', ({ credential }) => {\n if (credential && credential.locale) {\n i18next.changeLanguage(credential.locale)\n }\n })\n\n /* \n Get user profile information from server. \n As soon as response received, auth.on('profile', ...) handlers start to work.\n It's very important point to build UI for the user.\n */\n await auth.profile()\n}\n"]}
@@ -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":"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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 { 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":";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,8BAA8B,CAAA;AAErC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAEtG;;;;;;;;;GASG;AAEI,IAAM,kBAAkB,GAAxB,MAAM,kBAAmB,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAA9D;;QAiDuB,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;QAChC,+GAA+G;QAC9F,UAAK,GAAyB,EAAE,CAAA;IAgJnD,CAAC;aA7MQ,WAAM,GAAG;QACd,iBAAiB;QACjB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2CF;KACF,AA9CY,CA8CZ;IAkBD,KAAK,CAAC,YAAY;QAChB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;;;OAmBT;YACD,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;YACjE,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;QACxC,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,aAAa,IAAI,EAAE,CAAA;QAC/C,yGAAyG;QACzG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,CAAA;IAC9G,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;;UAEL,IAAI,CAAC,YAAY,EAAE;UACnB,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;IAED,uHAAuH;IAC/G,YAAY;QAClB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACjC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA,eAAe,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAA;QAC/G,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;YAC9C,OAAO,IAAI,CAAA,eAAe,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;UACvD,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA,aAAa,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;QACjD,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CACnB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAA;;2CAEyB,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,YAAY,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;;0BAEzF,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC;cACpC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA,aAAa,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;;;OAGxF,CACF,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;wBACnB,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;;AA5J2B;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;AAEf;IAAhB,KAAK,EAAE;;iDAAyC;AAChC;IAAhB,KAAK,EAAE;;kDAAwB;AA/DrB,kBAAkB;IAD9B,aAAa,CAAC,sBAAsB,CAAC;GACzB,kBAAkB,CA8M9B","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"]}
@@ -37,21 +37,21 @@ let MyLoginHistory = class MyLoginHistory extends LitElement {
37
37
  { type: 'gutter', gutterName: 'sequence' },
38
38
  {
39
39
  type: 'object',
40
- name: 'accessDomain',
40
+ name: 'domain',
41
41
  header: i18next.t('field.domain'),
42
42
  record: { editable: false },
43
43
  width: 200
44
44
  },
45
45
  {
46
46
  type: 'datetime',
47
- name: 'accessedAt',
47
+ name: 'createdAt',
48
48
  header: i18next.t('field.accessed-at'),
49
49
  record: { editable: false },
50
50
  width: 200
51
51
  },
52
52
  {
53
53
  type: 'string',
54
- name: 'accessorIp',
54
+ name: 'ip',
55
55
  header: i18next.t('field.ip_address'),
56
56
  record: { editable: false },
57
57
  width: 200
@@ -71,23 +71,27 @@ let MyLoginHistory = class MyLoginHistory extends LitElement {
71
71
  }
72
72
  async fetchLoginHistories() {
73
73
  try {
74
+ /* One's own check-ins, from the audit (ADR-0141 §8) - the address is the request's. */
74
75
  const response = await client.query({
75
76
  query: gql `
76
- query myLoginHistories($limit: Float!) {
77
- myLoginHistories(limit: $limit) {
78
- accessDomain {
77
+ query ($limit: Int) {
78
+ myAuditActions(kind: "auth.checkin", limit: $limit) {
79
+ domain {
79
80
  name
80
81
  }
81
- accessorIp
82
- accessedAt
82
+ createdAt
83
+ request {
84
+ ip
85
+ }
83
86
  }
84
87
  }
85
88
  `,
86
- variables: { limit: this.limit || 10 }
89
+ variables: { limit: this.limit || 10 },
90
+ fetchPolicy: 'no-cache'
87
91
  });
88
92
  if (response.errors?.length)
89
93
  return;
90
- this.histories = response.data.myLoginHistories;
94
+ this.histories = response.data.myAuditActions.map((one) => ({ ...one, ip: one.request?.ip }));
91
95
  }
92
96
  catch (e) {
93
97
  this.showToast('message' in e ? e.message : e);
@@ -1 +1 @@
1
- {"version":3,"file":"my-login-history.js","sourceRoot":"","sources":["../../client/components/my-login-history.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { isMobileDevice } from '@operato/utils'\nimport { ScrollbarStyles } from '@operato/styles'\n\n@customElement('my-login-history')\nclass MyLoginHistory extends LitElement {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n overflow: auto;\n }\n ox-grist {\n flex: 1;\n }\n `\n ]\n\n @property({ type: Array }) histories: any[] = []\n @property({ type: Number }) limit?: number\n\n render() {\n if (!this.histories?.length) return html``\n\n const config = {\n rows: { appendable: false },\n pagination: { infinite: true },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n {\n type: 'object',\n name: 'accessDomain',\n header: i18next.t('field.domain'),\n record: { editable: false },\n width: 200\n },\n {\n type: 'datetime',\n name: 'accessedAt',\n header: i18next.t('field.accessed-at'),\n record: { editable: false },\n width: 200\n },\n {\n type: 'string',\n name: 'accessorIp',\n header: i18next.t('field.ip_address'),\n record: { editable: false },\n width: 200\n }\n ]\n }\n\n return html`\n <ox-grist\n .mode=${isMobileDevice() ? 'LIST' : 'GRID'}\n .config=${config}\n .data=\"${{ records: this.histories }}\"\n ></ox-grist>\n `\n }\n\n firstUpdated() {\n this.fetchLoginHistories()\n }\n\n async fetchLoginHistories() {\n try {\n const response = await client.query({\n query: gql`\n query myLoginHistories($limit: Float!) {\n myLoginHistories(limit: $limit) {\n accessDomain {\n name\n }\n accessorIp\n accessedAt\n }\n }\n `,\n variables: { limit: this.limit || 10 }\n })\n\n if (response.errors?.length) return\n this.histories = response.data.myLoginHistories\n } catch (e: any) {\n this.showToast('message' in e ? e.message : e)\n }\n }\n\n showToast(message) {\n document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))\n }\n}\n"]}
1
+ {"version":3,"file":"my-login-history.js","sourceRoot":"","sources":["../../client/components/my-login-history.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { isMobileDevice } from '@operato/utils'\nimport { ScrollbarStyles } from '@operato/styles'\n\n@customElement('my-login-history')\nclass MyLoginHistory extends LitElement {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n overflow: auto;\n }\n ox-grist {\n flex: 1;\n }\n `\n ]\n\n @property({ type: Array }) histories: any[] = []\n @property({ type: Number }) limit?: number\n\n render() {\n if (!this.histories?.length) return html``\n\n const config = {\n rows: { appendable: false },\n pagination: { infinite: true },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n {\n type: 'object',\n name: 'domain',\n header: i18next.t('field.domain'),\n record: { editable: false },\n width: 200\n },\n {\n type: 'datetime',\n name: 'createdAt',\n header: i18next.t('field.accessed-at'),\n record: { editable: false },\n width: 200\n },\n {\n type: 'string',\n name: 'ip',\n header: i18next.t('field.ip_address'),\n record: { editable: false },\n width: 200\n }\n ]\n }\n\n return html`\n <ox-grist\n .mode=${isMobileDevice() ? 'LIST' : 'GRID'}\n .config=${config}\n .data=\"${{ records: this.histories }}\"\n ></ox-grist>\n `\n }\n\n firstUpdated() {\n this.fetchLoginHistories()\n }\n\n async fetchLoginHistories() {\n try {\n /* One's own check-ins, from the audit (ADR-0141 §8) - the address is the request's. */\n const response = await client.query({\n query: gql`\n query ($limit: Int) {\n myAuditActions(kind: \"auth.checkin\", limit: $limit) {\n domain {\n name\n }\n createdAt\n request {\n ip\n }\n }\n }\n `,\n variables: { limit: this.limit || 10 },\n fetchPolicy: 'no-cache'\n })\n\n if (response.errors?.length) return\n this.histories = response.data.myAuditActions.map((one: any) => ({ ...one, ip: one.request?.ip }))\n } catch (e: any) {\n this.showToast('message' in e ? e.message : e)\n }\n }\n\n showToast(message) {\n document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))\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"]}
@@ -75,6 +75,11 @@ export declare class RoleTemplatePicker extends RoleTemplatePicker_base {
75
75
  * (굵기·줄 간격·가운뎃점)은 아무것도 주장하지 않으므로 그대로 둔다.
76
76
  */
77
77
  private renderOpens;
78
+ /**
79
+ * The work this seat does, by task (ADR-0139 decision 11) — named in the words of the module that declares each.
80
+ * A template the framework refuses to apply says why here, before anyone presses create.
81
+ */
82
+ private renderTasks;
78
83
  private pick;
79
84
  private pickScratch;
80
85
  private canCreate;