@things-factory/auth-ui 10.1.194 → 10.1.202
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/bootstrap.js +4 -1
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/access-decision-popup.js +7 -5
- package/dist-client/components/access-decision-popup.js.map +1 -1
- package/dist-client/components/access-request-popup.d.ts +9 -1
- package/dist-client/components/access-request-popup.js +55 -4
- package/dist-client/components/access-request-popup.js.map +1 -1
- package/dist-client/components/my-login-history.js +14 -10
- package/dist-client/components/my-login-history.js.map +1 -1
- package/dist-client/components/role-task-edit.d.ts +22 -0
- package/dist-client/components/role-task-edit.js +17 -0
- package/dist-client/components/role-task-edit.js.map +1 -0
- package/dist-client/components/role-task-edit.test.d.ts +1 -0
- package/dist-client/components/role-task-edit.test.js +34 -0
- package/dist-client/components/role-task-edit.test.js.map +1 -0
- package/dist-client/components/role-template-picker.d.ts +5 -0
- package/dist-client/components/role-template-picker.js +41 -5
- package/dist-client/components/role-template-picker.js.map +1 -1
- package/dist-client/components/user-status-renderer.d.ts +9 -0
- package/dist-client/components/user-status-renderer.js +36 -0
- package/dist-client/components/user-status-renderer.js.map +1 -0
- package/dist-client/entity360/person-360.d.ts +13 -0
- package/dist-client/entity360/person-360.js +96 -0
- package/dist-client/entity360/person-360.js.map +1 -1
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +14 -1
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/access-request/access-requests.js +4 -2
- package/dist-client/pages/access-request/access-requests.js.map +1 -1
- package/dist-client/pages/access-request/my-access-requests.js +3 -2
- package/dist-client/pages/access-request/my-access-requests.js.map +1 -1
- package/dist-client/pages/audit/audit-page.d.ts +46 -0
- package/dist-client/pages/audit/audit-page.js +417 -0
- package/dist-client/pages/audit/audit-page.js.map +1 -0
- package/dist-client/pages/role/role-privilege-page.d.ts +46 -0
- package/dist-client/pages/role/role-privilege-page.js +500 -5
- package/dist-client/pages/role/role-privilege-page.js.map +1 -1
- package/dist-client/pages/role/role-task-review.test.d.ts +1 -0
- package/dist-client/pages/role/role-task-review.test.js +60 -0
- package/dist-client/pages/role/role-task-review.test.js.map +1 -0
- package/dist-client/pages/user/user-list-page.js +1 -31
- package/dist-client/pages/user/user-list-page.js.map +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/access-request.d.ts +15 -1
- package/dist-client/utils/access-request.js +19 -2
- package/dist-client/utils/access-request.js.map +1 -1
- package/dist-client/utils/access-request.test.js +26 -1
- package/dist-client/utils/access-request.test.js.map +1 -1
- package/dist-client/utils/audit-words.d.ts +30 -0
- package/dist-client/utils/audit-words.js +57 -0
- package/dist-client/utils/audit-words.js.map +1 -0
- package/dist-client/utils/audit-words.test.d.ts +1 -0
- package/dist-client/utils/audit-words.test.js +47 -0
- package/dist-client/utils/audit-words.test.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/things-factory.config.js +1 -0
- package/translations/en.json +78 -1
- package/translations/es.json +78 -1
- package/translations/ja.json +78 -1
- package/translations/ko.json +78 -1
- package/translations/ms.json +78 -1
- package/translations/zh.json +78 -1
package/dist-client/bootstrap.js
CHANGED
|
@@ -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":"AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,0BAA0B,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,iCAAiC,CAAA,CAAC,sDAAsD;AAE/F,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAE/B,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,wDAAwD,CAAA;AAC/F,OAAO,EACL,cAAc,IAAI,mBAAmB,EACrC,gBAAgB,IAAI,qBAAqB,EACzC,oBAAoB,EACrB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,IAAI,EAAE,MAAM,+CAA+C,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;
|
|
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,
|
|
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>${
|
|
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).
|
|
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
|
|
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).
|
|
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
|
-
|
|
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":"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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"]}
|
|
@@ -37,21 +37,21 @@ let MyLoginHistory = class MyLoginHistory extends LitElement {
|
|
|
37
37
|
{ type: 'gutter', gutterName: 'sequence' },
|
|
38
38
|
{
|
|
39
39
|
type: 'object',
|
|
40
|
-
name: '
|
|
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: '
|
|
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: '
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
query ($limit: Int) {
|
|
78
|
+
myAuditActions(kind: "auth.checkin", limit: $limit) {
|
|
79
|
+
domain {
|
|
79
80
|
name
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
|
|
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.
|
|
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":";AAAA,OAAO,qBAAqB,CAAA;AAE5B,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGjD,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,UAAU;IAAvC;;QAiB6B,cAAS,GAAU,EAAE,CAAA;
|
|
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;
|