@things-factory/auth-ui 10.1.104 → 10.1.107
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 +22 -1
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/access-decision-popup.d.ts +30 -0
- package/dist-client/components/access-decision-popup.js +228 -0
- package/dist-client/components/access-decision-popup.js.map +1 -0
- package/dist-client/components/access-request-popup.d.ts +32 -0
- package/dist-client/components/access-request-popup.js +211 -0
- package/dist-client/components/access-request-popup.js.map +1 -0
- package/dist-client/pages/access-request/access-requests.d.ts +51 -0
- package/dist-client/pages/access-request/access-requests.js +220 -0
- package/dist-client/pages/access-request/access-requests.js.map +1 -0
- package/dist-client/pages/access-request/my-access-requests.d.ts +26 -0
- package/dist-client/pages/access-request/my-access-requests.js +151 -0
- package/dist-client/pages/access-request/my-access-requests.js.map +1 -0
- package/dist-client/route.js +6 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.layout-check.tsbuildinfo +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/access-request.d.ts +49 -0
- package/dist-client/utils/access-request.js +67 -0
- package/dist-client/utils/access-request.js.map +1 -0
- package/dist-client/utils/access-request.test.d.ts +1 -0
- package/dist-client/utils/access-request.test.js +70 -0
- package/dist-client/utils/access-request.test.js.map +1 -0
- package/package.json +6 -6
- package/things-factory.config.js +3 -1
- package/translations/en.json +61 -1
- package/translations/ja.json +61 -1
- package/translations/ko.json +61 -1
- package/translations/ms.json +61 -1
- package/translations/zh.json +61 -1
package/dist-client/bootstrap.js
CHANGED
|
@@ -4,12 +4,13 @@ import { i18next } from '@operato/i18n';
|
|
|
4
4
|
import '@operato/attribute/grist-editor'; /* for register data-grist editor type 'attributes' */
|
|
5
5
|
import { html } from 'lit-html';
|
|
6
6
|
import { navigate, removeDraftsOf, store } from '@operato/shell';
|
|
7
|
-
import { TOOL_POSITION } from '@operato/layout';
|
|
7
|
+
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
11
|
import { ADD_MORENDA } from '@things-factory/more-base/client';
|
|
12
12
|
import { registerPersonKind } from './entity360/person-360.js';
|
|
13
|
+
import { requestableFrom } from './utils/access-request.js';
|
|
13
14
|
export default async function bootstrap() {
|
|
14
15
|
/* the person 360 - a User value anywhere opens it (ADR-0067) */
|
|
15
16
|
registerPersonKind();
|
|
@@ -67,6 +68,26 @@ export default async function bootstrap() {
|
|
|
67
68
|
if (user)
|
|
68
69
|
await removeDraftsOf(user).catch(() => { });
|
|
69
70
|
});
|
|
71
|
+
/*
|
|
72
|
+
* A refusal a role would open, of a change the person made, gets one quiet "request access…" on its toast. The toast
|
|
73
|
+
* does not know what an error means and @operato/graphql only reports it; this package knows, so it puts the button
|
|
74
|
+
* on (registerNotifyAction, things-factory ADR-0103 decision 5). Where auth-ui is not loaded there is no button.
|
|
75
|
+
*/
|
|
76
|
+
registerNotifyAction('access-request', notification => {
|
|
77
|
+
const refusal = Array.isArray(notification.ex) ? notification.ex[0] : undefined;
|
|
78
|
+
const detail = requestableFrom(refusal?.extensions, Array.isArray(refusal?.path) ? String(refusal.path[0]) : undefined);
|
|
79
|
+
if (!detail)
|
|
80
|
+
return null;
|
|
81
|
+
return {
|
|
82
|
+
label: i18next.t('access-request.button.request'),
|
|
83
|
+
emphasis: 'quiet',
|
|
84
|
+
callback: () => import('./components/access-request-popup.js').then(() => openPopup(html `<access-request-popup
|
|
85
|
+
.category=${detail.category}
|
|
86
|
+
.privilege=${detail.privilege}
|
|
87
|
+
.operationName=${detail.operationName}
|
|
88
|
+
></access-request-popup>`, { size: 'small', title: i18next.t('access-request.title.request access') }))
|
|
89
|
+
};
|
|
90
|
+
});
|
|
70
91
|
auth.on('profile', ({ credential }) => {
|
|
71
92
|
if (credential && credential.locale) {
|
|
72
93
|
i18next.changeLanguage(credential.locale);
|
|
@@ -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,aAAa,EAAE,MAAM,iBAAiB,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 { 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: 'small', 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"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import '@operato/popup/ox-popup-actions.js';
|
|
2
|
+
import '@material/web/button/text-button.js';
|
|
3
|
+
import '@material/web/button/filled-button.js';
|
|
4
|
+
import '@material/web/radio/radio.js';
|
|
5
|
+
import '@material/web/textfield/outlined-text-field.js';
|
|
6
|
+
import { LitElement } from 'lit';
|
|
7
|
+
declare const AccessDecisionPopup_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
8
|
+
/**
|
|
9
|
+
* Deciding one access request (ADR-0103 · docs/design/access-request.md §6-1).
|
|
10
|
+
*
|
|
11
|
+
* The findings are the server's review - the same function the AI draft card and the decision door read - so
|
|
12
|
+
* this popup never judges a role itself. Each change of role or reason asks again. A blocking finding takes
|
|
13
|
+
* the button away; a warning stays beside it.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AccessDecisionPopup extends AccessDecisionPopup_base {
|
|
16
|
+
static styles: import("lit").CSSResult[];
|
|
17
|
+
request: any;
|
|
18
|
+
private candidates;
|
|
19
|
+
private roleId?;
|
|
20
|
+
private reason;
|
|
21
|
+
private grantIssues;
|
|
22
|
+
private denyIssues;
|
|
23
|
+
private asked;
|
|
24
|
+
firstUpdated(): Promise<void>;
|
|
25
|
+
/** Asks the server again; an answer to an older question is dropped. */
|
|
26
|
+
private review;
|
|
27
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
28
|
+
private decide;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@operato/popup/ox-popup-actions.js';
|
|
3
|
+
import '@material/web/button/text-button.js';
|
|
4
|
+
import '@material/web/button/filled-button.js';
|
|
5
|
+
import '@material/web/radio/radio.js';
|
|
6
|
+
import '@material/web/textfield/outlined-text-field.js';
|
|
7
|
+
import gql from 'graphql-tag';
|
|
8
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
9
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
10
|
+
import { client, gqlContext } from '@operato/graphql';
|
|
11
|
+
import { i18next, localize } from '@operato/i18n';
|
|
12
|
+
import { CommonPopupStyles, ScrollbarStyles } from '@operato/styles';
|
|
13
|
+
import { decisionButtons, firstCandidate, issueWords, privilegeWords } from '../utils/access-request.js';
|
|
14
|
+
const REVIEW = gql `
|
|
15
|
+
query ($decision: AccessDecisionInput!) {
|
|
16
|
+
reviewAccessDecision(decision: $decision) {
|
|
17
|
+
code
|
|
18
|
+
blocking
|
|
19
|
+
field
|
|
20
|
+
params
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
/**
|
|
25
|
+
* Deciding one access request (ADR-0103 · docs/design/access-request.md §6-1).
|
|
26
|
+
*
|
|
27
|
+
* The findings are the server's review - the same function the AI draft card and the decision door read - so
|
|
28
|
+
* this popup never judges a role itself. Each change of role or reason asks again. A blocking finding takes
|
|
29
|
+
* the button away; a warning stays beside it.
|
|
30
|
+
*/
|
|
31
|
+
let AccessDecisionPopup = class AccessDecisionPopup extends localize(i18next)(LitElement) {
|
|
32
|
+
constructor() {
|
|
33
|
+
super(...arguments);
|
|
34
|
+
this.candidates = [];
|
|
35
|
+
this.reason = '';
|
|
36
|
+
this.grantIssues = null;
|
|
37
|
+
this.denyIssues = null;
|
|
38
|
+
this.asked = 0;
|
|
39
|
+
}
|
|
40
|
+
static { this.styles = [
|
|
41
|
+
CommonPopupStyles,
|
|
42
|
+
ScrollbarStyles,
|
|
43
|
+
css `
|
|
44
|
+
:host {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
background-color: var(--md-sys-color-surface);
|
|
48
|
+
}
|
|
49
|
+
[body] {
|
|
50
|
+
flex: 1;
|
|
51
|
+
overflow: auto;
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: var(--spacing-medium);
|
|
55
|
+
padding: var(--spacing-large);
|
|
56
|
+
}
|
|
57
|
+
[who],
|
|
58
|
+
[why] {
|
|
59
|
+
font: var(--label-font, inherit);
|
|
60
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
61
|
+
}
|
|
62
|
+
[needed] {
|
|
63
|
+
font: var(--title-font, inherit);
|
|
64
|
+
}
|
|
65
|
+
label[candidate] {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: var(--spacing-small);
|
|
69
|
+
}
|
|
70
|
+
[count] {
|
|
71
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
72
|
+
}
|
|
73
|
+
[issue][blocking] {
|
|
74
|
+
color: var(--md-sys-color-error);
|
|
75
|
+
}
|
|
76
|
+
[issue]:not([blocking]) {
|
|
77
|
+
color: var(--md-sys-color-tertiary);
|
|
78
|
+
}
|
|
79
|
+
[none] {
|
|
80
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
81
|
+
}
|
|
82
|
+
md-outlined-text-field {
|
|
83
|
+
width: 100%;
|
|
84
|
+
}
|
|
85
|
+
`
|
|
86
|
+
]; }
|
|
87
|
+
async firstUpdated() {
|
|
88
|
+
const response = await client.query({
|
|
89
|
+
query: gql `
|
|
90
|
+
query ($category: String!, $privilege: String!) {
|
|
91
|
+
rolesGranting(category: $category, privilege: $privilege) {
|
|
92
|
+
id
|
|
93
|
+
name
|
|
94
|
+
privilegeCount
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
`,
|
|
98
|
+
variables: { category: this.request.category, privilege: this.request.privilege },
|
|
99
|
+
context: gqlContext(),
|
|
100
|
+
fetchPolicy: 'network-only'
|
|
101
|
+
});
|
|
102
|
+
this.candidates = response.data?.rolesGranting || [];
|
|
103
|
+
this.roleId = firstCandidate(this.candidates)?.id;
|
|
104
|
+
await this.review();
|
|
105
|
+
}
|
|
106
|
+
/** Asks the server again; an answer to an older question is dropped. */
|
|
107
|
+
async review() {
|
|
108
|
+
const asked = ++this.asked;
|
|
109
|
+
const ask = (decision) => client.query({ query: REVIEW, variables: { decision }, context: gqlContext(), fetchPolicy: 'network-only' });
|
|
110
|
+
const [grant, deny] = await Promise.all([
|
|
111
|
+
ask({ requestId: this.request.id, decision: 'GRANT', roleId: this.roleId }),
|
|
112
|
+
ask({ requestId: this.request.id, decision: 'DENY', reason: this.reason })
|
|
113
|
+
]);
|
|
114
|
+
if (asked !== this.asked)
|
|
115
|
+
return;
|
|
116
|
+
this.grantIssues = grant.errors?.length ? null : grant.data?.reviewAccessDecision || [];
|
|
117
|
+
this.denyIssues = deny.errors?.length ? null : deny.data?.reviewAccessDecision || [];
|
|
118
|
+
}
|
|
119
|
+
render() {
|
|
120
|
+
const t = i18next.t.bind(i18next);
|
|
121
|
+
const { request } = this;
|
|
122
|
+
const buttons = decisionButtons(this.grantIssues, this.denyIssues);
|
|
123
|
+
const reasonIssues = (this.denyIssues || []).filter(issue => issue.field === 'reason');
|
|
124
|
+
return html `
|
|
125
|
+
<div body>
|
|
126
|
+
<div who>${t('access-request.text.who asks', { name: request.requester?.name || request.requester?.email || '' })}</div>
|
|
127
|
+
<div needed>${privilegeWords(t, request.category, request.privilege)}</div>
|
|
128
|
+
${request.reason ? html `<div why>${request.reason}</div>` : nothing}
|
|
129
|
+
|
|
130
|
+
<div>${t('access-request.text.grant with role')}</div>
|
|
131
|
+
${this.candidates.length === 0
|
|
132
|
+
? html `<div none>${t('access-request.text.no role grants it')}</div>`
|
|
133
|
+
: this.candidates.map(candidate => html `
|
|
134
|
+
<label candidate>
|
|
135
|
+
<md-radio
|
|
136
|
+
name="role"
|
|
137
|
+
.checked=${candidate.id === this.roleId}
|
|
138
|
+
@change=${() => {
|
|
139
|
+
this.roleId = candidate.id;
|
|
140
|
+
this.review();
|
|
141
|
+
}}
|
|
142
|
+
></md-radio>
|
|
143
|
+
<span>${candidate.name}</span>
|
|
144
|
+
<span count>${t('access-request.text.privilege count', { count: candidate.privilegeCount })}</span>
|
|
145
|
+
</label>
|
|
146
|
+
`)}
|
|
147
|
+
${(this.grantIssues || []).map(issue => html `<div issue ?blocking=${issue.blocking}>${issueWords(t, issue)}</div>`)}
|
|
148
|
+
|
|
149
|
+
<md-outlined-text-field
|
|
150
|
+
label=${t('access-request.field.deny reason')}
|
|
151
|
+
type="textarea"
|
|
152
|
+
rows="2"
|
|
153
|
+
.value=${this.reason}
|
|
154
|
+
@input=${(e) => {
|
|
155
|
+
this.reason = e.target.value;
|
|
156
|
+
this.review();
|
|
157
|
+
}}
|
|
158
|
+
></md-outlined-text-field>
|
|
159
|
+
${reasonIssues.length && this.reason ? reasonIssues.map(issue => html `<div issue blocking>${issueWords(t, issue)}</div>`) : nothing}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<ox-popup-actions>
|
|
163
|
+
<md-text-button slot="cancel" @click=${() => history.back()}>${t('button.cancel')}</md-text-button>
|
|
164
|
+
<md-text-button slot="start" ?disabled=${!buttons.deny} @click=${() => this.decide('DENY')}>
|
|
165
|
+
${t('access-request.button.deny')}
|
|
166
|
+
</md-text-button>
|
|
167
|
+
<md-filled-button slot="confirm" ?disabled=${!buttons.grant} @click=${() => this.decide('GRANT')}>
|
|
168
|
+
${t('access-request.button.grant')}
|
|
169
|
+
</md-filled-button>
|
|
170
|
+
</ox-popup-actions>
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
async decide(decision) {
|
|
174
|
+
const response = await client.mutate({
|
|
175
|
+
mutation: gql `
|
|
176
|
+
mutation ($decision: AccessDecisionInput!) {
|
|
177
|
+
decideAccessRequest(decision: $decision) {
|
|
178
|
+
id
|
|
179
|
+
state
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
`,
|
|
183
|
+
variables: {
|
|
184
|
+
decision: decision === 'GRANT'
|
|
185
|
+
? { requestId: this.request.id, decision, roleId: this.roleId }
|
|
186
|
+
: { requestId: this.request.id, decision, reason: this.reason }
|
|
187
|
+
},
|
|
188
|
+
context: gqlContext()
|
|
189
|
+
});
|
|
190
|
+
if (!response.errors?.length) {
|
|
191
|
+
this.dispatchEvent(new CustomEvent('access-request-decided', { bubbles: true, composed: true }));
|
|
192
|
+
history.back();
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
/* Somebody may have decided meanwhile - show what the server says now. */
|
|
196
|
+
await this.review();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
__decorate([
|
|
201
|
+
property({ type: Object }),
|
|
202
|
+
__metadata("design:type", Object)
|
|
203
|
+
], AccessDecisionPopup.prototype, "request", void 0);
|
|
204
|
+
__decorate([
|
|
205
|
+
state(),
|
|
206
|
+
__metadata("design:type", Array)
|
|
207
|
+
], AccessDecisionPopup.prototype, "candidates", void 0);
|
|
208
|
+
__decorate([
|
|
209
|
+
state(),
|
|
210
|
+
__metadata("design:type", String)
|
|
211
|
+
], AccessDecisionPopup.prototype, "roleId", void 0);
|
|
212
|
+
__decorate([
|
|
213
|
+
state(),
|
|
214
|
+
__metadata("design:type", Object)
|
|
215
|
+
], AccessDecisionPopup.prototype, "reason", void 0);
|
|
216
|
+
__decorate([
|
|
217
|
+
state(),
|
|
218
|
+
__metadata("design:type", Object)
|
|
219
|
+
], AccessDecisionPopup.prototype, "grantIssues", void 0);
|
|
220
|
+
__decorate([
|
|
221
|
+
state(),
|
|
222
|
+
__metadata("design:type", Object)
|
|
223
|
+
], AccessDecisionPopup.prototype, "denyIssues", void 0);
|
|
224
|
+
AccessDecisionPopup = __decorate([
|
|
225
|
+
customElement('access-decision-popup')
|
|
226
|
+
], AccessDecisionPopup);
|
|
227
|
+
export { AccessDecisionPopup };
|
|
228
|
+
//# sourceMappingURL=access-decision-popup.js.map
|
|
@@ -0,0 +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, 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 background-color: var(--md-sys-color-surface);\n }\n [body] {\n flex: 1;\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 ${(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"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import '@operato/popup/ox-popup-actions.js';
|
|
2
|
+
import '@material/web/button/text-button.js';
|
|
3
|
+
import '@material/web/button/filled-button.js';
|
|
4
|
+
import '@material/web/textfield/outlined-text-field.js';
|
|
5
|
+
import { LitElement } from 'lit';
|
|
6
|
+
declare const AccessRequestPopup_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
7
|
+
/**
|
|
8
|
+
* Asking for a privilege the person was just refused (ADR-0103, docs/design/access-request.md §3-2).
|
|
9
|
+
*
|
|
10
|
+
* What was needed comes from the refusal - the person writes only why, and must (questions Q9). Before sending
|
|
11
|
+
* the popup says the request is recorded and how many people it reaches; a denial of the last days is shown with
|
|
12
|
+
* its reason, and the same words are not sent again. A request already waiting is shown instead of a blank form:
|
|
13
|
+
* pressing again adds to it, the people deciding are not told twice.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AccessRequestPopup extends AccessRequestPopup_base {
|
|
16
|
+
static styles: import("lit").CSSResult[];
|
|
17
|
+
category: string;
|
|
18
|
+
privilege: string;
|
|
19
|
+
operationName?: string;
|
|
20
|
+
/** The request of this person and privilege that is still waiting, if any. */
|
|
21
|
+
private waiting;
|
|
22
|
+
/** A denial of the same privilege in the last days - its reason is shown before the person writes (Q9 · 5). */
|
|
23
|
+
private denied;
|
|
24
|
+
/** How many people a request reaches - said before sending (Q9 · 2). */
|
|
25
|
+
private deciders;
|
|
26
|
+
private reason;
|
|
27
|
+
private sending;
|
|
28
|
+
firstUpdated(): Promise<void>;
|
|
29
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
30
|
+
private send;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@operato/popup/ox-popup-actions.js';
|
|
3
|
+
import '@material/web/button/text-button.js';
|
|
4
|
+
import '@material/web/button/filled-button.js';
|
|
5
|
+
import '@material/web/textfield/outlined-text-field.js';
|
|
6
|
+
import gql from 'graphql-tag';
|
|
7
|
+
import { css, html, LitElement } from 'lit';
|
|
8
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
9
|
+
import { client, gqlContext } from '@operato/graphql';
|
|
10
|
+
import { i18next, localize } from '@operato/i18n';
|
|
11
|
+
import { CommonPopupStyles } from '@operato/styles';
|
|
12
|
+
import { maySend, privilegeWords, recentDenial } from '../utils/access-request.js';
|
|
13
|
+
/**
|
|
14
|
+
* Asking for a privilege the person was just refused (ADR-0103, docs/design/access-request.md §3-2).
|
|
15
|
+
*
|
|
16
|
+
* What was needed comes from the refusal - the person writes only why, and must (questions Q9). Before sending
|
|
17
|
+
* the popup says the request is recorded and how many people it reaches; a denial of the last days is shown with
|
|
18
|
+
* its reason, and the same words are not sent again. A request already waiting is shown instead of a blank form:
|
|
19
|
+
* pressing again adds to it, the people deciding are not told twice.
|
|
20
|
+
*/
|
|
21
|
+
let AccessRequestPopup = class AccessRequestPopup extends localize(i18next)(LitElement) {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
this.category = '';
|
|
25
|
+
this.privilege = '';
|
|
26
|
+
/** The request of this person and privilege that is still waiting, if any. */
|
|
27
|
+
this.waiting = null;
|
|
28
|
+
/** A denial of the same privilege in the last days - its reason is shown before the person writes (Q9 · 5). */
|
|
29
|
+
this.denied = null;
|
|
30
|
+
/** How many people a request reaches - said before sending (Q9 · 2). */
|
|
31
|
+
this.deciders = null;
|
|
32
|
+
this.reason = '';
|
|
33
|
+
this.sending = false;
|
|
34
|
+
}
|
|
35
|
+
static { this.styles = [
|
|
36
|
+
CommonPopupStyles,
|
|
37
|
+
css `
|
|
38
|
+
:host {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
background-color: var(--md-sys-color-surface);
|
|
42
|
+
}
|
|
43
|
+
[body] {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: var(--spacing-medium);
|
|
47
|
+
padding: var(--spacing-large);
|
|
48
|
+
}
|
|
49
|
+
[needed] {
|
|
50
|
+
font: var(--title-font, inherit);
|
|
51
|
+
color: var(--md-sys-color-on-surface);
|
|
52
|
+
}
|
|
53
|
+
[operation],
|
|
54
|
+
[waiting],
|
|
55
|
+
[recorded] {
|
|
56
|
+
font: var(--label-font, inherit);
|
|
57
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
58
|
+
}
|
|
59
|
+
[denied] {
|
|
60
|
+
font: var(--label-font, inherit);
|
|
61
|
+
color: var(--md-sys-color-error);
|
|
62
|
+
}
|
|
63
|
+
md-outlined-text-field {
|
|
64
|
+
width: 100%;
|
|
65
|
+
}
|
|
66
|
+
`
|
|
67
|
+
]; }
|
|
68
|
+
async firstUpdated() {
|
|
69
|
+
const response = await client.query({
|
|
70
|
+
query: gql `
|
|
71
|
+
query {
|
|
72
|
+
myAccessRequests {
|
|
73
|
+
id
|
|
74
|
+
category
|
|
75
|
+
privilege
|
|
76
|
+
state
|
|
77
|
+
reason
|
|
78
|
+
count
|
|
79
|
+
lastRequestedAt
|
|
80
|
+
decidedAt
|
|
81
|
+
decisionReason
|
|
82
|
+
}
|
|
83
|
+
accessRequestDeciderCount
|
|
84
|
+
}
|
|
85
|
+
`,
|
|
86
|
+
context: gqlContext(),
|
|
87
|
+
fetchPolicy: 'network-only'
|
|
88
|
+
});
|
|
89
|
+
const mine = response.data?.myAccessRequests || [];
|
|
90
|
+
this.waiting = mine.find((r) => r.state === 'PENDING' && r.category === this.category && r.privilege === this.privilege) || null;
|
|
91
|
+
this.denied = this.waiting ? null : recentDenial(mine, this.category, this.privilege) || null;
|
|
92
|
+
this.deciders = typeof response.data?.accessRequestDeciderCount === 'number' ? response.data.accessRequestDeciderCount : null;
|
|
93
|
+
this.reason = this.waiting?.reason || '';
|
|
94
|
+
}
|
|
95
|
+
render() {
|
|
96
|
+
return html `
|
|
97
|
+
<div body>
|
|
98
|
+
<div needed>${privilegeWords(i18next.t.bind(i18next), this.category, this.privilege)}</div>
|
|
99
|
+
${this.operationName
|
|
100
|
+
? html `<div operation>${i18next.t('access-request.text.refused at', { operation: this.operationName })}</div>`
|
|
101
|
+
: ''}
|
|
102
|
+
${this.waiting
|
|
103
|
+
? html `<div waiting>
|
|
104
|
+
${i18next.t('access-request.text.already waiting', { at: new Date(this.waiting.lastRequestedAt).toLocaleString() })}
|
|
105
|
+
</div>`
|
|
106
|
+
: ''}
|
|
107
|
+
${this.denied
|
|
108
|
+
? html `<div denied>
|
|
109
|
+
${i18next.t('access-request.text.denied lately', {
|
|
110
|
+
at: new Date(this.denied.decidedAt).toLocaleDateString(),
|
|
111
|
+
reason: this.denied.decisionReason || ''
|
|
112
|
+
})}
|
|
113
|
+
</div>`
|
|
114
|
+
: ''}
|
|
115
|
+
<md-outlined-text-field
|
|
116
|
+
label=${i18next.t('access-request.field.reason')}
|
|
117
|
+
required
|
|
118
|
+
type="textarea"
|
|
119
|
+
rows="2"
|
|
120
|
+
.value=${this.reason}
|
|
121
|
+
@input=${(e) => (this.reason = e.target.value)}
|
|
122
|
+
></md-outlined-text-field>
|
|
123
|
+
${this.deciders === null
|
|
124
|
+
? ''
|
|
125
|
+
: html `<div recorded>
|
|
126
|
+
${this.deciders > 0
|
|
127
|
+
? i18next.t('access-request.text.recorded and told', { count: this.deciders })
|
|
128
|
+
: i18next.t('access-request.text.nobody to tell')}
|
|
129
|
+
</div>`}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<ox-popup-actions>
|
|
133
|
+
<md-text-button slot="cancel" @click=${() => history.back()}>${i18next.t('button.cancel')}</md-text-button>
|
|
134
|
+
<md-filled-button slot="confirm" ?disabled=${this.sending || !maySend(this.reason, this.denied) || this.deciders === 0} @click=${this.send}>
|
|
135
|
+
${i18next.t('access-request.button.send')}
|
|
136
|
+
</md-filled-button>
|
|
137
|
+
</ox-popup-actions>
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
async send() {
|
|
141
|
+
this.sending = true;
|
|
142
|
+
try {
|
|
143
|
+
const response = await client.mutate({
|
|
144
|
+
mutation: gql `
|
|
145
|
+
mutation ($request: NewAccessRequest!) {
|
|
146
|
+
requestAccess(request: $request) {
|
|
147
|
+
id
|
|
148
|
+
count
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`,
|
|
152
|
+
variables: {
|
|
153
|
+
request: {
|
|
154
|
+
category: this.category,
|
|
155
|
+
privilege: this.privilege,
|
|
156
|
+
operationName: this.operationName,
|
|
157
|
+
reason: this.reason.trim()
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
context: gqlContext()
|
|
161
|
+
});
|
|
162
|
+
if (!response.errors?.length) {
|
|
163
|
+
const merged = (response.data?.requestAccess?.count || 1) > 1;
|
|
164
|
+
document.dispatchEvent(new CustomEvent('notify', {
|
|
165
|
+
detail: { level: 'info', message: i18next.t(merged ? 'access-request.text.added to waiting' : 'access-request.text.sent') }
|
|
166
|
+
}));
|
|
167
|
+
history.back();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
this.sending = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
__decorate([
|
|
176
|
+
property({ type: String }),
|
|
177
|
+
__metadata("design:type", String)
|
|
178
|
+
], AccessRequestPopup.prototype, "category", void 0);
|
|
179
|
+
__decorate([
|
|
180
|
+
property({ type: String }),
|
|
181
|
+
__metadata("design:type", String)
|
|
182
|
+
], AccessRequestPopup.prototype, "privilege", void 0);
|
|
183
|
+
__decorate([
|
|
184
|
+
property({ type: String }),
|
|
185
|
+
__metadata("design:type", String)
|
|
186
|
+
], AccessRequestPopup.prototype, "operationName", void 0);
|
|
187
|
+
__decorate([
|
|
188
|
+
state(),
|
|
189
|
+
__metadata("design:type", Object)
|
|
190
|
+
], AccessRequestPopup.prototype, "waiting", void 0);
|
|
191
|
+
__decorate([
|
|
192
|
+
state(),
|
|
193
|
+
__metadata("design:type", Object)
|
|
194
|
+
], AccessRequestPopup.prototype, "denied", void 0);
|
|
195
|
+
__decorate([
|
|
196
|
+
state(),
|
|
197
|
+
__metadata("design:type", Object)
|
|
198
|
+
], AccessRequestPopup.prototype, "deciders", void 0);
|
|
199
|
+
__decorate([
|
|
200
|
+
state(),
|
|
201
|
+
__metadata("design:type", Object)
|
|
202
|
+
], AccessRequestPopup.prototype, "reason", void 0);
|
|
203
|
+
__decorate([
|
|
204
|
+
state(),
|
|
205
|
+
__metadata("design:type", Object)
|
|
206
|
+
], AccessRequestPopup.prototype, "sending", void 0);
|
|
207
|
+
AccessRequestPopup = __decorate([
|
|
208
|
+
customElement('access-request-popup')
|
|
209
|
+
], AccessRequestPopup);
|
|
210
|
+
export { AccessRequestPopup };
|
|
211
|
+
//# sourceMappingURL=access-request-popup.js.map
|
|
@@ -0,0 +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 background-color: var(--md-sys-color-surface);\n }\n [body] {\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"]}
|