@things-factory/auth-ui 10.1.31 → 10.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist-client/components/accept-invitation.d.ts +48 -0
  2. package/dist-client/components/accept-invitation.js +265 -0
  3. package/dist-client/components/accept-invitation.js.map +1 -0
  4. package/dist-client/components/appliance-editor.d.ts +3 -0
  5. package/dist-client/components/appliance-editor.js +102 -1
  6. package/dist-client/components/appliance-editor.js.map +1 -1
  7. package/dist-client/components/role-selector.js +23 -1
  8. package/dist-client/components/role-selector.js.map +1 -1
  9. package/dist-client/components/user-invite.d.ts +8 -0
  10. package/dist-client/components/user-invite.js +53 -6
  11. package/dist-client/components/user-invite.js.map +1 -1
  12. package/dist-client/entries/auth/accept-invitation.d.ts +3 -0
  13. package/dist-client/entries/auth/accept-invitation.js +10 -0
  14. package/dist-client/entries/auth/accept-invitation.js.map +1 -0
  15. package/dist-client/pages/invitation/invitation-list-query.test.d.ts +1 -0
  16. package/dist-client/pages/invitation/invitation-list-query.test.js +69 -0
  17. package/dist-client/pages/invitation/invitation-list-query.test.js.map +1 -0
  18. package/dist-client/pages/invitation/invitations.d.ts +63 -0
  19. package/dist-client/pages/invitation/invitations.js +282 -0
  20. package/dist-client/pages/invitation/invitations.js.map +1 -0
  21. package/dist-client/pages/role/role-list-page.d.ts +3 -2
  22. package/dist-client/pages/role/role-list-page.js +39 -6
  23. package/dist-client/pages/role/role-list-page.js.map +1 -1
  24. package/dist-client/route.js +3 -0
  25. package/dist-client/route.js.map +1 -1
  26. package/dist-client/tsconfig.tsbuildinfo +1 -1
  27. package/dist-client/utils/credential-panel-state.d.ts +51 -0
  28. package/dist-client/utils/credential-panel-state.js +49 -0
  29. package/dist-client/utils/credential-panel-state.js.map +1 -0
  30. package/dist-client/utils/credential-panel-state.test.d.ts +1 -0
  31. package/dist-client/utils/credential-panel-state.test.js +88 -0
  32. package/dist-client/utils/credential-panel-state.test.js.map +1 -0
  33. package/dist-client/utils/inherited-name-refusal.d.ts +44 -0
  34. package/dist-client/utils/inherited-name-refusal.js +46 -0
  35. package/dist-client/utils/inherited-name-refusal.js.map +1 -0
  36. package/dist-client/utils/inherited-name-refusal.test.d.ts +1 -0
  37. package/dist-client/utils/inherited-name-refusal.test.js +60 -0
  38. package/dist-client/utils/inherited-name-refusal.test.js.map +1 -0
  39. package/dist-client/utils/invitation-token.d.ts +28 -0
  40. package/dist-client/utils/invitation-token.js +40 -0
  41. package/dist-client/utils/invitation-token.js.map +1 -0
  42. package/dist-client/utils/invitation-token.test.d.ts +1 -0
  43. package/dist-client/utils/invitation-token.test.js +60 -0
  44. package/dist-client/utils/invitation-token.test.js.map +1 -0
  45. package/dist-client/utils/role-origin.d.ts +44 -0
  46. package/dist-client/utils/role-origin.js +47 -0
  47. package/dist-client/utils/role-origin.js.map +1 -0
  48. package/dist-client/utils/role-origin.test.d.ts +1 -0
  49. package/dist-client/utils/role-origin.test.js +60 -0
  50. package/dist-client/utils/role-origin.test.js.map +1 -0
  51. package/package.json +5 -5
  52. package/things-factory.config.js +1 -0
  53. package/translations/en.json +28 -0
  54. package/translations/ja.json +28 -0
  55. package/translations/ko.json +28 -0
  56. package/translations/ms.json +28 -0
  57. package/translations/zh.json +28 -0
@@ -0,0 +1,63 @@
1
+ import '@material/web/icon/icon.js';
2
+ import '@operato/data-grist/ox-grist.js';
3
+ import type { FetchOption } from '@operato/data-grist';
4
+ import { PageView } from '@operato/shell';
5
+ declare const Invitations_base: (new (...args: any[]) => {
6
+ __preferenceProviders: {
7
+ [element: string]: import("@operato/p13n").PagePreferenceProvider;
8
+ };
9
+ getPagePreferenceProvider(element: string): import("@operato/p13n").PagePreferenceProvider | undefined;
10
+ }) & (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
11
+ /**
12
+ * Who was invited into this domain, and what became of it.
13
+ *
14
+ * ── Why this screen is the reason the table exists ──────────────────────────
15
+ * Accepting used to delete the row, so the moment somebody joined, the fact that they had been
16
+ * invited stopped existing. There was nothing to put on a screen — which is why no screen was
17
+ * built, and why nothing called any of the invitation doors at all. The rows stay now and carry
18
+ * a state, and this is what that is for.
19
+ *
20
+ * ── ⚠ The token is not a column and cannot be ───────────────────────────────
21
+ * The token in an invitation mail is what opens the door. This is a screen an administrator
22
+ * reads, so printing it here would hand every pending invitation to whoever can open the list,
23
+ * and to any screenshot of it. It is not a GraphQL field on `Invitation` at all — the same
24
+ * reason `app-bindings` does not ask for a refresh token.
25
+ *
26
+ * ── The state is read as of now ─────────────────────────────────────────────
27
+ * `state` comes from the server's field resolver, which measures a pending row against the
28
+ * clock rather than trusting the stored column. A row whose expiry passed an hour ago says
29
+ * expired here even though nothing has been round to write that down — showing it as live would
30
+ * be the more dangerous of the two ways to be wrong.
31
+ */
32
+ export declare class Invitations extends Invitations_base {
33
+ static styles: import("lit").CSSResult[];
34
+ active: boolean;
35
+ config: any;
36
+ mode: 'CARD' | 'LIST' | 'GRID';
37
+ /** The last fetch did not answer. Distinct from answering with nothing. */
38
+ failed: boolean;
39
+ private grist;
40
+ get context(): {
41
+ title: string;
42
+ help: string;
43
+ search: {
44
+ handler: (search: string) => void;
45
+ value: string;
46
+ };
47
+ actions: {
48
+ title: string;
49
+ action: () => void;
50
+ icon: string;
51
+ }[];
52
+ };
53
+ render(): import("lit-html").TemplateResult<1>;
54
+ pageInitialized(): Promise<void>;
55
+ private resend;
56
+ private withdraw;
57
+ pageUpdated(_changes: any, _lifecycle: any): Promise<void>;
58
+ fetchHandler({ page, limit, sortings, filters }: FetchOption): Promise<{
59
+ total: number;
60
+ records: any[];
61
+ }>;
62
+ }
63
+ export {};
@@ -0,0 +1,282 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@operato/data-grist/ox-grist.js';
4
+ import gql from 'graphql-tag';
5
+ import { css, html } from 'lit';
6
+ import { customElement, property, query } from 'lit/decorators.js';
7
+ import { client, gqlContext } from '@operato/graphql';
8
+ import { i18next, localize } from '@operato/i18n';
9
+ import { OxPrompt } from '@operato/popup/ox-prompt.js';
10
+ import { navigate, PageView } from '@operato/shell';
11
+ import { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
12
+ import { isMobileDevice } from '@operato/utils';
13
+ import { p13n } from '@operato/p13n';
14
+ import { listOutcome, pageFromResponse } from '../../utils/list-result.js';
15
+ /**
16
+ * Who was invited into this domain, and what became of it.
17
+ *
18
+ * ── Why this screen is the reason the table exists ──────────────────────────
19
+ * Accepting used to delete the row, so the moment somebody joined, the fact that they had been
20
+ * invited stopped existing. There was nothing to put on a screen — which is why no screen was
21
+ * built, and why nothing called any of the invitation doors at all. The rows stay now and carry
22
+ * a state, and this is what that is for.
23
+ *
24
+ * ── ⚠ The token is not a column and cannot be ───────────────────────────────
25
+ * The token in an invitation mail is what opens the door. This is a screen an administrator
26
+ * reads, so printing it here would hand every pending invitation to whoever can open the list,
27
+ * and to any screenshot of it. It is not a GraphQL field on `Invitation` at all — the same
28
+ * reason `app-bindings` does not ask for a refresh token.
29
+ *
30
+ * ── The state is read as of now ─────────────────────────────────────────────
31
+ * `state` comes from the server's field resolver, which measures a pending row against the
32
+ * clock rather than trusting the stored column. A row whose expiry passed an hour ago says
33
+ * expired here even though nothing has been round to write that down — showing it as live would
34
+ * be the more dangerous of the two ways to be wrong.
35
+ */
36
+ let Invitations = class Invitations extends p13n(localize(i18next)(PageView)) {
37
+ constructor() {
38
+ super(...arguments);
39
+ this.active = false;
40
+ this.mode = isMobileDevice() ? 'CARD' : 'GRID';
41
+ /** The last fetch did not answer. Distinct from answering with nothing. */
42
+ this.failed = false;
43
+ }
44
+ static { this.styles = [
45
+ WorkSurfacePageStyles,
46
+ CommonGristStyles,
47
+ CommonHeaderStyles,
48
+ ScrollbarStyles,
49
+ css `
50
+ [failed] {
51
+ padding: var(--spacing-medium);
52
+ color: var(--md-sys-color-error);
53
+ font: var(--label-font, inherit);
54
+ }
55
+ `
56
+ ]; }
57
+ get context() {
58
+ return {
59
+ title: i18next.t('label.invitations'),
60
+ help: 'auth/user',
61
+ search: {
62
+ handler: (search) => {
63
+ this.grist.searchText = search;
64
+ },
65
+ value: this.grist?.searchText || ''
66
+ },
67
+ actions: [
68
+ {
69
+ title: i18next.t('text.users'),
70
+ action: () => navigate('users'),
71
+ icon: 'group'
72
+ }
73
+ ]
74
+ };
75
+ }
76
+ render() {
77
+ return html `
78
+ <ox-grist
79
+ class="page-fill"
80
+ .mode=${this.mode}
81
+ .config=${this.config}
82
+ .fetchHandler=${this.fetchHandler.bind(this)}
83
+ .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')}
84
+ >
85
+ </ox-grist>
86
+
87
+ ${this.failed ? html `<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}
88
+ `;
89
+ }
90
+ async pageInitialized() {
91
+ this.config = {
92
+ rows: { appendable: false, selectable: { multiple: false } },
93
+ columns: [
94
+ { type: 'gutter', gutterName: 'sequence' },
95
+ {
96
+ type: 'string',
97
+ name: 'email',
98
+ header: i18next.t('field.email'),
99
+ record: { editable: false },
100
+ sortable: true,
101
+ width: 240
102
+ },
103
+ {
104
+ type: 'string',
105
+ name: 'state',
106
+ header: i18next.t('field.state'),
107
+ record: { align: 'center', editable: false },
108
+ width: 120
109
+ },
110
+ {
111
+ type: 'string',
112
+ name: 'roleNames',
113
+ header: i18next.t('field.roles'),
114
+ record: { editable: false },
115
+ width: 200
116
+ },
117
+ {
118
+ type: 'string',
119
+ name: 'creatorName',
120
+ header: i18next.t('field.invited'),
121
+ record: { editable: false },
122
+ width: 160
123
+ },
124
+ {
125
+ type: 'datetime',
126
+ name: 'createdAt',
127
+ header: i18next.t('field.created at'),
128
+ sortable: true,
129
+ width: 180
130
+ },
131
+ {
132
+ type: 'datetime',
133
+ name: 'expiresAt',
134
+ header: i18next.t('field.expires at'),
135
+ sortable: true,
136
+ width: 180
137
+ },
138
+ {
139
+ type: 'datetime',
140
+ name: 'acceptedAt',
141
+ header: i18next.t('field.accepted at'),
142
+ sortable: true,
143
+ width: 180
144
+ },
145
+ /*
146
+ * Both actions only mean anything while an invitation is still open, so they are offered
147
+ * only there. A button that is always present and refuses on half the rows reads as the
148
+ * screen being broken rather than as the row being finished.
149
+ */
150
+ {
151
+ type: 'gutter',
152
+ gutterName: 'button',
153
+ icon: 'refresh',
154
+ title: i18next.t('button.resend'),
155
+ handlers: {
156
+ click: (_c, _d, _col, record) => record?.state === 'pending' && this.resend(record)
157
+ }
158
+ },
159
+ {
160
+ type: 'gutter',
161
+ gutterName: 'button',
162
+ icon: 'block',
163
+ title: i18next.t('button.withdraw'),
164
+ handlers: {
165
+ click: (_c, _d, _col, record) => record?.state === 'pending' && this.withdraw(record)
166
+ }
167
+ }
168
+ ]
169
+ };
170
+ }
171
+ async resend(record) {
172
+ /*
173
+ * A fresh token each time. Re-mailing the old one would leave two live links for one
174
+ * invitation, and the older mail is the one more likely to have been forwarded.
175
+ */
176
+ const response = await client.mutate({
177
+ mutation: gql `
178
+ mutation ($id: String!) {
179
+ resendInvitation(id: $id)
180
+ }
181
+ `,
182
+ variables: { id: record.id },
183
+ context: gqlContext()
184
+ });
185
+ if (!response.errors?.length) {
186
+ document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'info', message: i18next.t('text.invitation resent') } }));
187
+ }
188
+ this.grist.fetch();
189
+ }
190
+ async withdraw(record) {
191
+ const confirmed = await OxPrompt.open({
192
+ type: 'warning',
193
+ title: i18next.t('button.withdraw'),
194
+ text: i18next.t('text.are you sure you want to delete x', { x: record.email }),
195
+ confirmButton: { text: i18next.t('button.withdraw') },
196
+ cancelButton: { text: i18next.t('button.cancel') }
197
+ });
198
+ if (!confirmed) {
199
+ return;
200
+ }
201
+ await client.mutate({
202
+ mutation: gql `
203
+ mutation ($id: String!) {
204
+ cancelInvitation(id: $id)
205
+ }
206
+ `,
207
+ variables: { id: record.id },
208
+ context: gqlContext()
209
+ });
210
+ this.grist.fetch();
211
+ }
212
+ async pageUpdated(_changes, _lifecycle) {
213
+ if (this.active) {
214
+ await this.updateComplete;
215
+ this.grist.fetch();
216
+ }
217
+ }
218
+ async fetchHandler({ page, limit, sortings = [], filters = [] }) {
219
+ const response = await client.query({
220
+ query: gql `
221
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
222
+ invitations(filters: $filters, pagination: $pagination, sortings: $sortings) {
223
+ items {
224
+ id
225
+ email
226
+ state
227
+ createdAt
228
+ expiresAt
229
+ acceptedAt
230
+ roles {
231
+ id
232
+ name
233
+ }
234
+ creator {
235
+ id
236
+ name
237
+ }
238
+ }
239
+ total
240
+ }
241
+ }
242
+ `,
243
+ variables: { filters, pagination: { page, limit }, sortings },
244
+ context: gqlContext()
245
+ });
246
+ const { items, total, failed } = listOutcome(pageFromResponse(response, 'invitations'));
247
+ this.failed = failed;
248
+ return {
249
+ total,
250
+ records: items.map((item) => ({
251
+ ...item,
252
+ roleNames: (item.roles || []).map((role) => role.name).join(', '),
253
+ creatorName: item.creator?.name || ''
254
+ }))
255
+ };
256
+ }
257
+ };
258
+ __decorate([
259
+ property({ type: Boolean }),
260
+ __metadata("design:type", Boolean)
261
+ ], Invitations.prototype, "active", void 0);
262
+ __decorate([
263
+ property({ type: Object }),
264
+ __metadata("design:type", Object)
265
+ ], Invitations.prototype, "config", void 0);
266
+ __decorate([
267
+ property({ type: String }),
268
+ __metadata("design:type", String)
269
+ ], Invitations.prototype, "mode", void 0);
270
+ __decorate([
271
+ property({ type: Boolean }),
272
+ __metadata("design:type", Boolean)
273
+ ], Invitations.prototype, "failed", void 0);
274
+ __decorate([
275
+ query('ox-grist'),
276
+ __metadata("design:type", Function)
277
+ ], Invitations.prototype, "grist", void 0);
278
+ Invitations = __decorate([
279
+ customElement('invitations-page')
280
+ ], Invitations);
281
+ export { Invitations };
282
+ //# sourceMappingURL=invitations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invitations.js","sourceRoot":"","sources":["../../../client/pages/invitation/invitations.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport '@operato/data-grist/ox-grist.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { p13n } from '@operato/p13n'\n\nimport { listOutcome, pageFromResponse } from '../../utils/list-result.js'\n\n/**\n * Who was invited into this domain, and what became of it.\n *\n * ── Why this screen is the reason the table exists ──────────────────────────\n * Accepting used to delete the row, so the moment somebody joined, the fact that they had been\n * invited stopped existing. There was nothing to put on a screen — which is why no screen was\n * built, and why nothing called any of the invitation doors at all. The rows stay now and carry\n * a state, and this is what that is for.\n *\n * ── ⚠ The token is not a column and cannot be ───────────────────────────────\n * The token in an invitation mail is what opens the door. This is a screen an administrator\n * reads, so printing it here would hand every pending invitation to whoever can open the list,\n * and to any screenshot of it. It is not a GraphQL field on `Invitation` at all — the same\n * reason `app-bindings` does not ask for a refresh token.\n *\n * ── The state is read as of now ─────────────────────────────────────────────\n * `state` comes from the server's field resolver, which measures a pending row against the\n * clock rather than trusting the stored column. A row whose expiry passed an hour ago says\n * expired here even though nothing has been round to write that down — showing it as live would\n * be the more dangerous of the two ways to be wrong.\n */\n@customElement('invitations-page')\nexport class Invitations extends p13n(localize(i18next)(PageView)) {\n static styles = [\n WorkSurfacePageStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n [failed] {\n padding: var(--spacing-medium);\n color: var(--md-sys-color-error);\n font: var(--label-font, inherit);\n }\n `\n ]\n\n @property({ type: Boolean }) active: boolean = false\n @property({ type: Object }) config: any\n @property({ type: String }) mode: 'CARD' | 'LIST' | 'GRID' = isMobileDevice() ? 'CARD' : 'GRID'\n\n /** The last fetch did not answer. Distinct from answering with nothing. */\n @property({ type: Boolean }) failed: boolean = false\n\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n return {\n title: i18next.t('label.invitations'),\n help: 'auth/user',\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n actions: [\n {\n title: i18next.t('text.users'),\n action: () => navigate('users'),\n icon: 'group'\n }\n ]\n }\n }\n\n render() {\n return html`\n <ox-grist\n class=\"page-fill\"\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n </ox-grist>\n\n ${this.failed ? html`<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}\n `\n }\n\n async pageInitialized() {\n this.config = {\n rows: { appendable: false, selectable: { multiple: false } },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n {\n type: 'string',\n name: 'email',\n header: i18next.t('field.email'),\n record: { editable: false },\n sortable: true,\n width: 240\n },\n {\n type: 'string',\n name: 'state',\n header: i18next.t('field.state'),\n record: { align: 'center', editable: false },\n width: 120\n },\n {\n type: 'string',\n name: 'roleNames',\n header: i18next.t('field.roles'),\n record: { editable: false },\n width: 200\n },\n {\n type: 'string',\n name: 'creatorName',\n header: i18next.t('field.invited'),\n record: { editable: false },\n width: 160\n },\n {\n type: 'datetime',\n name: 'createdAt',\n header: i18next.t('field.created at'),\n sortable: true,\n width: 180\n },\n {\n type: 'datetime',\n name: 'expiresAt',\n header: i18next.t('field.expires at'),\n sortable: true,\n width: 180\n },\n {\n type: 'datetime',\n name: 'acceptedAt',\n header: i18next.t('field.accepted at'),\n sortable: true,\n width: 180\n },\n /*\n * Both actions only mean anything while an invitation is still open, so they are offered\n * only there. A button that is always present and refuses on half the rows reads as the\n * screen being broken rather than as the row being finished.\n */\n {\n type: 'gutter',\n gutterName: 'button',\n icon: 'refresh',\n title: i18next.t('button.resend'),\n handlers: {\n click: (_c: any, _d: any, _col: any, record: any) => record?.state === 'pending' && this.resend(record)\n }\n },\n {\n type: 'gutter',\n gutterName: 'button',\n icon: 'block',\n title: i18next.t('button.withdraw'),\n handlers: {\n click: (_c: any, _d: any, _col: any, record: any) => record?.state === 'pending' && this.withdraw(record)\n }\n }\n ]\n }\n }\n\n private async resend(record: any) {\n /*\n * A fresh token each time. Re-mailing the old one would leave two live links for one\n * invitation, and the older mail is the one more likely to have been forwarded.\n */\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n resendInvitation(id: $id)\n }\n `,\n variables: { id: record.id },\n context: gqlContext()\n })\n\n if (!response.errors?.length) {\n document.dispatchEvent(\n new CustomEvent('notify', { detail: { level: 'info', message: i18next.t('text.invitation resent') } })\n )\n }\n\n this.grist.fetch()\n }\n\n private async withdraw(record: any) {\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.withdraw'),\n text: i18next.t('text.are you sure you want to delete x', { x: record.email }),\n confirmButton: { text: i18next.t('button.withdraw') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n cancelInvitation(id: $id)\n }\n `,\n variables: { id: record.id },\n context: gqlContext()\n })\n\n this.grist.fetch()\n }\n\n async pageUpdated(_changes: any, _lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n }\n }\n\n async fetchHandler({ page, limit, sortings = [], filters = [] }: FetchOption) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n invitations(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n email\n state\n createdAt\n expiresAt\n acceptedAt\n roles {\n id\n name\n }\n creator {\n id\n name\n }\n }\n total\n }\n }\n `,\n variables: { filters, pagination: { page, limit }, sortings },\n context: gqlContext()\n })\n\n const { items, total, failed } = listOutcome(pageFromResponse(response, 'invitations'))\n\n this.failed = failed\n\n return {\n total,\n records: items.map((item: any) => ({\n ...item,\n roleNames: (item.roles || []).map((role: any) => role.name).join(', '),\n creatorName: item.creator?.name || ''\n }))\n }\n }\n}\n"]}
@@ -209,9 +209,10 @@ export declare class RoleListPage extends RoleListPageBase {
209
209
  */
210
210
  private openCreate;
211
211
  /** 템플릿에서 — 이름은 이 공장이 정한 것을 그대로 보낸다. */
212
- createFromTemplate(templateId: string, name: string): Promise<boolean>;
212
+ createFromTemplate(templateId: string, name: string, separately?: boolean): any;
213
+ private confirmStandingSeparately;
213
214
  /** 백지에서 — 권한 없이 선다. 목록의 권한 수 칸이 0 을 빨갛게 말하므로 다음 걸음이 보인다. */
214
- createBlankRole(name: string, description: string): Promise<boolean>;
215
+ createBlankRole(name: string, description: string): Promise<any>;
215
216
  /**
216
217
  * 새 역할.
217
218
  *
@@ -14,6 +14,7 @@ import { InheritedValueType, navigate, PageView } from '@operato/shell';
14
14
  import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
15
15
  import { isMobileDevice } from '@operato/utils';
16
16
  import { getDomain } from '@things-factory/auth-base/dist-client';
17
+ import { standsInheritedRefusal } from '../../utils/inherited-name-refusal.js';
17
18
  /**
18
19
  * 역할 목록 — **보이는 것과 고칠 수 있는 것을 가른다.**
19
20
  *
@@ -777,25 +778,54 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
777
778
  });
778
779
  }
779
780
  /** 템플릿에서 — 이름은 이 공장이 정한 것을 그대로 보낸다. */
780
- async createFromTemplate(templateId, name) {
781
+ async createFromTemplate(templateId, name, separately) {
781
782
  const response = await client.mutate({
782
783
  mutation: gql `
783
- mutation ($templateId: String!, $name: String) {
784
- createRoleFromTemplate(templateId: $templateId, name: $name) {
784
+ mutation ($templateId: String!, $name: String, $separately: Boolean) {
785
+ createRoleFromTemplate(templateId: $templateId, name: $name, separately: $separately) {
785
786
  id
786
787
  }
787
788
  }
788
789
  `,
789
- variables: { templateId, name },
790
+ variables: { templateId, name, separately },
790
791
  context: gqlContext()
791
792
  });
792
793
  if (response.errors?.length) {
794
+ if (await this.confirmStandingSeparately(response)) {
795
+ return await this.createFromTemplate(templateId, name, true);
796
+ }
793
797
  document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
794
798
  return false;
795
799
  }
796
800
  this.grist.fetch();
797
801
  return true;
798
802
  }
803
+ /*
804
+ * ── The one refusal this screen can offer a way out of ──────────────────
805
+ * A role name already visible through the parent domain is refused so that it cannot happen
806
+ * silently, not because it must not happen: standing up the same names here is the first step
807
+ * of moving a domain off an inherited structure (ADR-0062 decisions 2 and 3). So the operator
808
+ * is told which domain the other one stands in and asked whether that is what they mean.
809
+ *
810
+ * The name does not change. Offering to rename instead would close the only route decision 2
811
+ * defines — the role standing up here is the one that replaces what is inherited, and cutting
812
+ * the parent last leaves one name again.
813
+ *
814
+ * Every other refusal returns false and is reported as it arrives.
815
+ */
816
+ async confirmStandingSeparately(response) {
817
+ const refusal = standsInheritedRefusal(response);
818
+ if (!refusal) {
819
+ return false;
820
+ }
821
+ return !!(await OxPrompt.open({
822
+ type: 'warning',
823
+ title: i18next.t('text.a role of this name is already visible'),
824
+ text: i18next.t('text.stand this domain own role of the same name', refusal),
825
+ confirmButton: { text: i18next.t('button.stand it here') },
826
+ cancelButton: { text: i18next.t('button.cancel') }
827
+ }));
828
+ }
799
829
  /** 백지에서 — 권한 없이 선다. 목록의 권한 수 칸이 0 을 빨갛게 말하므로 다음 걸음이 보인다. */
800
830
  async createBlankRole(name, description) {
801
831
  return await this.onCreateRole({ name, description });
@@ -806,7 +836,7 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
806
836
  * **권한 없이 만들어진다.** 그것이 맞다 — 이름을 정하는 것과 무엇을 열지 정하는 것은 다른
807
837
  * 판단이고, 목록의 요약 칸이 「여는 것이 없음」을 빨갛게 말하므로 다음 걸음이 보인다.
808
838
  */
809
- async onCreateRole(record) {
839
+ async onCreateRole(record, separately) {
810
840
  const response = await client.mutate({
811
841
  mutation: gql `
812
842
  mutation ($role: NewRole!) {
@@ -815,10 +845,13 @@ let RoleListPage = class RoleListPage extends RoleListPageBase {
815
845
  }
816
846
  }
817
847
  `,
818
- variables: { role: { name: record.name, description: record.description } },
848
+ variables: { role: { name: record.name, description: record.description, separately } },
819
849
  context: gqlContext()
820
850
  });
821
851
  if (response.errors?.length) {
852
+ if (await this.confirmStandingSeparately(response)) {
853
+ return await this.onCreateRole(record, true);
854
+ }
822
855
  document.dispatchEvent(new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } }));
823
856
  return false;
824
857
  }