@things-factory/auth-ui 10.1.13 → 10.1.17

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 (83) hide show
  1. package/dist-client/components/role-template-picker.js +6 -8
  2. package/dist-client/components/role-template-picker.js.map +1 -1
  3. package/dist-client/components/secret-field.d.ts +35 -0
  4. package/dist-client/components/secret-field.js +149 -0
  5. package/dist-client/components/secret-field.js.map +1 -0
  6. package/dist-client/pages/app-binding/app-binding-list-query.test.d.ts +1 -0
  7. package/dist-client/pages/app-binding/app-binding-list-query.test.js +46 -0
  8. package/dist-client/pages/app-binding/app-binding-list-query.test.js.map +1 -0
  9. package/dist-client/pages/app-binding/app-binding.d.ts +1 -0
  10. package/dist-client/pages/app-binding/app-binding.js +8 -8
  11. package/dist-client/pages/app-binding/app-binding.js.map +1 -1
  12. package/dist-client/pages/app-binding/app-bindings.d.ts +63 -2
  13. package/dist-client/pages/app-binding/app-bindings.js +199 -117
  14. package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
  15. package/dist-client/pages/appliance/appliance-list-query.test.d.ts +1 -0
  16. package/dist-client/pages/appliance/appliance-list-query.test.js +58 -0
  17. package/dist-client/pages/appliance/appliance-list-query.test.js.map +1 -0
  18. package/dist-client/pages/appliance/appliance.d.ts +1 -0
  19. package/dist-client/pages/appliance/appliance.js +2 -4
  20. package/dist-client/pages/appliance/appliance.js.map +1 -1
  21. package/dist-client/pages/appliance/home.d.ts +61 -2
  22. package/dist-client/pages/appliance/home.js +200 -122
  23. package/dist-client/pages/appliance/home.js.map +1 -1
  24. package/dist-client/pages/application/application.d.ts +1 -0
  25. package/dist-client/pages/application/application.js +10 -3
  26. package/dist-client/pages/application/application.js.map +1 -1
  27. package/dist-client/pages/application/applications.d.ts +61 -2
  28. package/dist-client/pages/application/applications.js +193 -128
  29. package/dist-client/pages/application/applications.js.map +1 -1
  30. package/dist-client/pages/application/register.js +81 -27
  31. package/dist-client/pages/application/register.js.map +1 -1
  32. package/dist-client/pages/auth-provider/auth-provider-management.d.ts +4 -2
  33. package/dist-client/pages/auth-provider/auth-provider-management.js +35 -6
  34. package/dist-client/pages/auth-provider/auth-provider-management.js.map +1 -1
  35. package/dist-client/pages/domain-owner/domain-owner-management.d.ts +2 -0
  36. package/dist-client/pages/domain-owner/domain-owner-management.js +34 -7
  37. package/dist-client/pages/domain-owner/domain-owner-management.js.map +1 -1
  38. package/dist-client/pages/role/role-list-page.d.ts +20 -0
  39. package/dist-client/pages/role/role-list-page.js +58 -1
  40. package/dist-client/pages/role/role-list-page.js.map +1 -1
  41. package/dist-client/pages/role/role-privilege-page.js +1 -10
  42. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  43. package/dist-client/pages/secrets-are-not-printed.test.d.ts +1 -0
  44. package/dist-client/pages/secrets-are-not-printed.test.js +70 -0
  45. package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -0
  46. package/dist-client/tsconfig.tsbuildinfo +1 -1
  47. package/dist-client/utils/application-status.d.ts +32 -0
  48. package/dist-client/utils/application-status.js +45 -0
  49. package/dist-client/utils/application-status.js.map +1 -0
  50. package/dist-client/utils/application-status.test.d.ts +1 -0
  51. package/dist-client/utils/application-status.test.js +45 -0
  52. package/dist-client/utils/application-status.test.js.map +1 -0
  53. package/dist-client/utils/form-values.d.ts +42 -0
  54. package/dist-client/utils/form-values.js +57 -0
  55. package/dist-client/utils/form-values.js.map +1 -0
  56. package/dist-client/utils/form-values.test.d.ts +1 -0
  57. package/dist-client/utils/form-values.test.js +62 -0
  58. package/dist-client/utils/form-values.test.js.map +1 -0
  59. package/dist-client/utils/list-result.d.ts +62 -0
  60. package/dist-client/utils/list-result.js +92 -0
  61. package/dist-client/utils/list-result.js.map +1 -0
  62. package/dist-client/utils/list-result.test.d.ts +1 -0
  63. package/dist-client/utils/list-result.test.js +160 -0
  64. package/dist-client/utils/list-result.test.js.map +1 -0
  65. package/dist-client/utils/privilege-axis.d.ts +52 -0
  66. package/dist-client/utils/privilege-axis.js +55 -0
  67. package/dist-client/utils/privilege-axis.js.map +1 -0
  68. package/dist-client/utils/privilege-axis.test.d.ts +1 -0
  69. package/dist-client/utils/privilege-axis.test.js +39 -0
  70. package/dist-client/utils/privilege-axis.test.js.map +1 -0
  71. package/dist-client/utils/secret-display.d.ts +51 -0
  72. package/dist-client/utils/secret-display.js +61 -0
  73. package/dist-client/utils/secret-display.js.map +1 -0
  74. package/dist-client/utils/secret-display.test.d.ts +1 -0
  75. package/dist-client/utils/secret-display.test.js +59 -0
  76. package/dist-client/utils/secret-display.test.js.map +1 -0
  77. package/dist-server/tsconfig.tsbuildinfo +1 -1
  78. package/package.json +4 -4
  79. package/translations/en.json +33 -0
  80. package/translations/ja.json +33 -0
  81. package/translations/ko.json +33 -0
  82. package/translations/ms.json +33 -0
  83. package/translations/zh.json +33 -0
@@ -1,160 +1,225 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
- import '@material/web/button/elevated-button.js';
3
- import '@material/web/button/outlined-button.js';
2
+ import '@material/web/icon/icon.js';
3
+ import '@operato/data-grist/ox-grist.js';
4
+ import '@operato/context/ox-context-page-toolbar.js';
4
5
  import gql from 'graphql-tag';
5
6
  import { css, html } from 'lit';
6
- import { customElement, property } from 'lit/decorators.js';
7
- import { client } from '@operato/graphql';
7
+ import { customElement, property, query } from 'lit/decorators.js';
8
+ import { client, gqlContext } from '@operato/graphql';
9
+ import { i18next, localize } from '@operato/i18n';
8
10
  import { navigate, PageView } from '@operato/shell';
9
- let Applications = class Applications extends PageView {
11
+ import { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles';
12
+ import { isMobileDevice } from '@operato/utils';
13
+ import { p13n } from '@operato/p13n';
14
+ import { applicationStatusLabel } from '../../utils/application-status.js';
15
+ import { listOutcome, pageFromResponse } from '../../utils/list-result.js';
16
+ /**
17
+ * Registered applications.
18
+ *
19
+ * ── What this screen looked like before ──────────────────────────────────────
20
+ * A hand-written `<table>` with no paging, no sort and no search, and three columns that
21
+ * were not data at all: "API health" printed OK, "Installs" printed 1, "status" printed
22
+ * draft — written into the markup, identical on every row. The previous commit removed the
23
+ * two the server cannot answer and made status real. This one gives the list the same shape
24
+ * as every other list in the product.
25
+ *
26
+ * The server was already ready for it: `applications` takes ListParam and declares
27
+ * `searchables: ['name', 'description']`, so paging, sorting and the header's search box all
28
+ * work by passing the grist's own options through.
29
+ *
30
+ * ── Why the row does not carry health or install counts ──────────────────────
31
+ * Because Application does not declare them. If they arrive one day, they are columns; until
32
+ * then the screen says what it knows. See `utils/application-status.ts` for the matching rule
33
+ * on values the screen has no word for.
34
+ */
35
+ let Applications = class Applications extends p13n(localize(i18next)(PageView)) {
10
36
  constructor() {
11
37
  super(...arguments);
12
- this.applications = [];
38
+ this.active = false;
39
+ this.mode = isMobileDevice() ? 'CARD' : 'GRID';
40
+ /** The last fetch did not answer. Distinct from answering with nothing. */
41
+ this.failed = false;
13
42
  }
14
43
  static { this.styles = [
44
+ CommonGristStyles,
45
+ CommonHeaderStyles,
46
+ ScrollbarStyles,
15
47
  css `
16
- :host { background-color: var(--md-sys-color-background);
17
- padding: var(--spacing-large);
18
-
19
- overflow: auto;
20
- }
21
-
22
- md-elevated-button { text-transform: capitalize;
23
- }
24
-
25
- md-outlined-button { float: right;
26
- margin-top: var(--spacing-medium);
27
- text-transform: capitalize;
28
- }
29
-
30
- h2 { margin: var(--title-margin);
31
- font: var(--title-font);
32
- color: var(--title-text-color);
33
- }
34
-
35
- [page-description] { margin: var(--page-description-margin);
36
- font: var(--page-description-font);
37
- color: var(--page-description-color);
38
- }
39
-
40
- table { margin: var(--spacing-large) 0;
41
- width: 100%;
42
- border-collapse: collapse;
43
- }
44
-
45
- tr { background-color: var(--tr-background-color);
46
- }
47
-
48
- tr:nth-child(odd) { background-color: var(--tr-background-odd-color);
49
- }
50
-
51
- tr:hover { background-color: var(--tr-background-hover-color);
52
- }
53
-
54
- th { border-top: var(--th-border-top);
55
- border-bottom: var(--td-border-bottom);
56
- padding: var(--th-padding);
57
-
58
- font: var(--th-font);
59
- color: var(--th-color);
60
- text-transform: var(--th-text-transform);
61
- text-align: left;
62
- }
63
-
64
- td { padding: var(--td-padding);
65
- border-bottom: var(--td-border-bottom);
66
- font: var(--td-font);
67
- color: var(--td-color);
68
- }
69
-
70
- td a { font: var(--td-font);
71
- text-decoration: none;
72
- color: var(--md-sys-color-on-surface);
73
- }
74
-
75
- td a strong { font: bold 16px var(--theme-font);
76
-
77
- display: block;
78
- text-decoration: none;
79
- }
80
-
81
- .text-align-center { text-align: center;
82
- }
83
-
84
- .text-align-right { text-align: right;
85
- }
48
+ :host {
49
+ display: flex;
50
+ flex-direction: column;
51
+ overflow: hidden;
52
+ }
53
+
54
+ ox-grist {
55
+ overflow-y: auto;
56
+ flex: 1;
57
+ }
58
+
59
+ [failed] {
60
+ padding: var(--spacing-medium);
61
+ color: var(--md-sys-color-error);
62
+ font: var(--label-font, inherit);
63
+ }
86
64
  `
87
65
  ]; }
88
66
  get context() {
89
- return { title: `applications`,
90
- help: 'auth/application'
67
+ return {
68
+ title: i18next.t('text.applications'),
69
+ help: 'auth/application',
70
+ search: {
71
+ handler: (search) => {
72
+ this.grist.searchText = search;
73
+ },
74
+ value: this.grist?.searchText || ''
75
+ },
76
+ actions: [
77
+ {
78
+ title: i18next.t('button.register'),
79
+ action: () => navigate('application-register'),
80
+ icon: 'add'
81
+ }
82
+ ],
83
+ toolbar: false
91
84
  };
92
85
  }
93
86
  render() {
94
- var apps = this.applications || [];
95
87
  return html `
96
- <div>
97
- <h2>Registered Applications</h2>
98
- <p page-description>
99
- What type of app are you building?<br />Choose the app type that best suits the audience you’re building for.
100
- </p>
101
-
102
- <md-elevated-button @click=${e => navigate('application-register')}
103
- >register new application</md-elevated-button
104
- >
105
- </div>
106
-
107
- <table>
108
- <tr>
109
- <th>app name</th>
110
- <th>API health</th>
111
- <th>Installs</th>
112
- <th>status</th>
113
- </tr>
114
- ${apps.map(app => html `
115
- <tr>
116
- <td>
117
- <a href=${`application/${app.id}`}>
118
- <strong>${app.name}</strong>
119
- ${app.description}
120
- </a>
121
- </td>
122
- <td>OK</td>
123
- <td class="text-align-center">1</td>
124
- <td>draft</td>
125
- </tr>
126
- `)}
127
- </table>
128
-
129
- <md-outlined-button @click=${e => navigate('app-bindings')}>bound applications ..</md-outlined-button>
88
+ <ox-grist
89
+ .mode=${this.mode}
90
+ .config=${this.config}
91
+ .fetchHandler=${this.fetchHandler.bind(this)}
92
+ .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')}
93
+ >
94
+ <div slot="headroom" class="header">
95
+ <ox-context-page-toolbar class="actions" .context=${this.context}></ox-context-page-toolbar>
96
+ </div>
97
+ </ox-grist>
98
+
99
+ ${this.failed ? html `<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}
130
100
  `;
131
101
  }
132
- async pageUpdated(changes, lifecycle, before) {
102
+ async pageInitialized() {
103
+ this.config = {
104
+ rows: { appendable: false, selectable: { multiple: false } },
105
+ columns: [
106
+ { type: 'gutter', gutterName: 'sequence' },
107
+ {
108
+ type: 'gutter',
109
+ gutterName: 'button',
110
+ icon: 'chevron_right',
111
+ title: i18next.t('button.detail'),
112
+ handlers: {
113
+ click: (_columns, _data, _column, record) => {
114
+ if (record?.id) {
115
+ navigate(`application/${record.id}`);
116
+ }
117
+ }
118
+ }
119
+ },
120
+ {
121
+ type: 'string',
122
+ name: 'name',
123
+ header: i18next.t('field.name'),
124
+ record: { editable: false },
125
+ sortable: true,
126
+ width: 200
127
+ },
128
+ {
129
+ type: 'string',
130
+ name: 'description',
131
+ header: i18next.t('field.description'),
132
+ record: { editable: false },
133
+ width: 260
134
+ },
135
+ {
136
+ type: 'string',
137
+ name: 'type',
138
+ header: i18next.t('field.type'),
139
+ record: { align: 'center', editable: false },
140
+ sortable: true,
141
+ width: 120
142
+ },
143
+ {
144
+ type: 'string',
145
+ name: 'statusLabel',
146
+ header: i18next.t('field.status'),
147
+ record: { align: 'center', editable: false },
148
+ width: 120
149
+ },
150
+ {
151
+ type: 'datetime',
152
+ name: 'updatedAt',
153
+ header: i18next.t('field.updated at'),
154
+ sortable: true,
155
+ width: 180
156
+ }
157
+ ]
158
+ };
159
+ }
160
+ async pageUpdated(_changes, _lifecycle) {
133
161
  if (this.active) {
134
- this.applications = (await this.fetchApplications()).items;
162
+ await this.updateComplete;
163
+ this.grist.fetch();
135
164
  }
136
165
  }
137
- async fetchApplications() {
138
- const response = await client.query({ query: gql `
139
- query { applications { items { id
166
+ async fetchHandler({ page, limit, sortings = [], filters = [] }) {
167
+ const response = await client.query({
168
+ query: gql `
169
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
170
+ applications(filters: $filters, pagination: $pagination, sortings: $sortings) {
171
+ items {
172
+ id
140
173
  name
141
174
  description
142
- }
175
+ type
176
+ status
177
+ updatedAt
178
+ }
143
179
  total
144
- }
145
- }
146
- `
147
- });
148
- if (!response.errors) {
149
- return response.data.applications;
180
+ }
150
181
  }
182
+ `,
183
+ variables: { filters, pagination: { page, limit }, sortings },
184
+ context: gqlContext()
185
+ });
186
+ const { items, total, failed } = listOutcome(pageFromResponse(response, 'applications'));
187
+ this.failed = failed;
188
+ /*
189
+ * The status is turned into words here rather than in a cell renderer so the rule lives
190
+ * in one tested place — a value this screen has no word for is printed as it arrived.
191
+ */
192
+ return {
193
+ total,
194
+ records: items.map((item) => ({
195
+ ...item,
196
+ statusLabel: applicationStatusLabel(item.status, (key) => i18next.t(key))
197
+ }))
198
+ };
151
199
  }
152
200
  };
153
201
  __decorate([
154
- property({ type: Array }),
155
- __metadata("design:type", Array)
156
- ], Applications.prototype, "applications", void 0);
202
+ property({ type: Boolean }),
203
+ __metadata("design:type", Boolean)
204
+ ], Applications.prototype, "active", void 0);
205
+ __decorate([
206
+ property({ type: Object }),
207
+ __metadata("design:type", Object)
208
+ ], Applications.prototype, "config", void 0);
209
+ __decorate([
210
+ property({ type: String }),
211
+ __metadata("design:type", String)
212
+ ], Applications.prototype, "mode", void 0);
213
+ __decorate([
214
+ property({ type: Boolean }),
215
+ __metadata("design:type", Boolean)
216
+ ], Applications.prototype, "failed", void 0);
217
+ __decorate([
218
+ query('ox-grist'),
219
+ __metadata("design:type", Function)
220
+ ], Applications.prototype, "grist", void 0);
157
221
  Applications = __decorate([
158
222
  customElement('applications-page')
159
223
  ], Applications);
224
+ export { Applications };
160
225
  //# sourceMappingURL=applications.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"applications.js","sourceRoot":"","sources":["../../../client/pages/application/applications.ts"],"names":[],"mappings":";AAAA,OAAO,yCAAyC,CAAA;AAChD,OAAO,yCAAyC,CAAA;AAEhD,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGnD,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,QAAQ;IAAnC;;QA2E6B,iBAAY,GAAU,EAAE,CAAA;IAmErD,CAAC;aA9I8C,WAAM,GAAG;QACpD,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAuEF;KACF,AAzEkD,CAyElD;IAID,IAAI,OAAO;QAAS,OAAO,EAAQ,KAAK,EAAE,cAAc;YACpD,IAAI,EAAE,kBAAkB;SAC5B,CAAA;IACD,CAAC;IAEA,MAAM;QAAS,IAAI,IAAI,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAA;QAE/C,OAAO,IAAI,CAAA;;;;;;;qCAOsB,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;;;;;;;;;;;;UAYhE,IAAI,CAAC,GAAG,CACR,GAAG,CAAC,EAAE,CAAC,IAAI,CAAA;;;0BAGK,eAAe,GAAG,CAAC,EAAE,EAAE;4BACrB,GAAG,CAAC,IAAI;oBAChB,GAAG,CAAC,WAAW;;;;;;;WAOxB,CACF;;;mCAG0B,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;KAC3D,CAAA;IACJ,CAAC;IAEA,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM;QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAAO,IAAI,CAAC,YAAY,GAAG,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAA;QACxI,CAAC;IACD,CAAC;IAEA,KAAK,CAAC,iBAAiB;QAAS,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,EAAQ,KAAK,EAAE,GAAG,CAAA;;;;;;;;OAQjF;SACL,CAAC,CAAA;QAEC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAAO,OAAO,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAA;QAClE,CAAC;IACD,CAAC;;AAlE2B;IAA1B,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;;kDAAyB;AA3E/C,YAAY;IADjB,aAAa,CAAC,mBAAmB,CAAC;GAC7B,YAAY,CA8IjB","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '@material/web/button/outlined-button.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { navigate, PageView } from '@operato/shell'\n\n@customElement('applications-page')\nclass Applications extends PageView { static styles = [\n css`\n :host { background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n\n overflow: auto;\n }\n\n md-elevated-button { text-transform: capitalize;\n }\n\n md-outlined-button { float: right;\n margin-top: var(--spacing-medium);\n text-transform: capitalize;\n }\n\n h2 { margin: var(--title-margin);\n font: var(--title-font);\n color: var(--title-text-color);\n }\n\n [page-description] { margin: var(--page-description-margin);\n font: var(--page-description-font);\n color: var(--page-description-color);\n }\n\n table { margin: var(--spacing-large) 0;\n width: 100%;\n border-collapse: collapse;\n }\n\n tr { background-color: var(--tr-background-color);\n }\n\n tr:nth-child(odd) { background-color: var(--tr-background-odd-color);\n }\n\n tr:hover { background-color: var(--tr-background-hover-color);\n }\n\n th { border-top: var(--th-border-top);\n border-bottom: var(--td-border-bottom);\n padding: var(--th-padding);\n\n font: var(--th-font);\n color: var(--th-color);\n text-transform: var(--th-text-transform);\n text-align: left;\n }\n\n td { padding: var(--td-padding);\n border-bottom: var(--td-border-bottom);\n font: var(--td-font);\n color: var(--td-color);\n }\n\n td a { font: var(--td-font);\n text-decoration: none;\n color: var(--md-sys-color-on-surface);\n }\n\n td a strong { font: bold 16px var(--theme-font);\n\n display: block;\n text-decoration: none;\n }\n\n .text-align-center { text-align: center;\n }\n\n .text-align-right { text-align: right;\n }\n `\n ]\n\n @property({ type: Array }) applications: any[] = []\n\n get context() { return { title: `applications`,\n help: 'auth/application'\n }\n }\n\n render() { var apps = this.applications || []\n\n return html`\n <div>\n <h2>Registered Applications</h2>\n <p page-description>\n What type of app are you building?<br />Choose the app type that best suits the audience you’re building for.\n </p>\n\n <md-elevated-button @click=${e => navigate('application-register')}\n >register new application</md-elevated-button\n >\n </div>\n\n <table>\n <tr>\n <th>app name</th>\n <th>API health</th>\n <th>Installs</th>\n <th>status</th>\n </tr>\n ${apps.map(\n app => html`\n <tr>\n <td>\n <a href=${`application/${app.id}`}>\n <strong>${app.name}</strong>\n ${app.description}\n </a>\n </td>\n <td>OK</td>\n <td class=\"text-align-center\">1</td>\n <td>draft</td>\n </tr>\n `\n )}\n </table>\n\n <md-outlined-button @click=${e => navigate('app-bindings')}>bound applications ..</md-outlined-button>\n `\n }\n\n async pageUpdated(changes, lifecycle, before) { if (this.active) { this.applications = (await this.fetchApplications()).items\n }\n }\n\n async fetchApplications() { const response = await client.query({ query: gql`\n query { applications { items { id\n name\n description\n }\n total\n }\n }\n `\n })\n\n if (!response.errors) { return response.data.applications\n }\n }\n}\n"]}
1
+ {"version":3,"file":"applications.js","sourceRoot":"","sources":["../../../client/pages/application/applications.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport '@operato/data-grist/ox-grist.js'\nimport '@operato/context/ox-context-page-toolbar.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 { navigate, PageView } from '@operato/shell'\nimport { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { p13n } from '@operato/p13n'\n\nimport { applicationStatusLabel } from '../../utils/application-status.js'\nimport { listOutcome, pageFromResponse } from '../../utils/list-result.js'\n\n/**\n * Registered applications.\n *\n * ── What this screen looked like before ──────────────────────────────────────\n * A hand-written `<table>` with no paging, no sort and no search, and three columns that\n * were not data at all: \"API health\" printed OK, \"Installs\" printed 1, \"status\" printed\n * draft — written into the markup, identical on every row. The previous commit removed the\n * two the server cannot answer and made status real. This one gives the list the same shape\n * as every other list in the product.\n *\n * The server was already ready for it: `applications` takes ListParam and declares\n * `searchables: ['name', 'description']`, so paging, sorting and the header's search box all\n * work by passing the grist's own options through.\n *\n * ── Why the row does not carry health or install counts ──────────────────────\n * Because Application does not declare them. If they arrive one day, they are columns; until\n * then the screen says what it knows. See `utils/application-status.ts` for the matching rule\n * on values the screen has no word for.\n */\n@customElement('applications-page')\nexport class Applications extends p13n(localize(i18next)(PageView)) {\n static styles = [\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n ox-grist {\n overflow-y: auto;\n flex: 1;\n }\n\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('text.applications'),\n help: 'auth/application',\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('button.register'),\n action: () => navigate('application-register'),\n icon: 'add'\n }\n ],\n toolbar: false\n }\n }\n\n render() {\n return html`\n <ox-grist\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <ox-context-page-toolbar class=\"actions\" .context=${this.context}></ox-context-page-toolbar>\n </div>\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: 'gutter',\n gutterName: 'button',\n icon: 'chevron_right',\n title: i18next.t('button.detail'),\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: any) => {\n if (record?.id) {\n navigate(`application/${record.id}`)\n }\n }\n }\n },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('field.name'),\n record: { editable: false },\n sortable: true,\n width: 200\n },\n {\n type: 'string',\n name: 'description',\n header: i18next.t('field.description'),\n record: { editable: false },\n width: 260\n },\n {\n type: 'string',\n name: 'type',\n header: i18next.t('field.type'),\n record: { align: 'center', editable: false },\n sortable: true,\n width: 120\n },\n {\n type: 'string',\n name: 'statusLabel',\n header: i18next.t('field.status'),\n record: { align: 'center', editable: false },\n width: 120\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated at'),\n sortable: true,\n width: 180\n }\n ]\n }\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 applications(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n name\n description\n type\n status\n updatedAt\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, 'applications'))\n\n this.failed = failed\n\n /*\n * The status is turned into words here rather than in a cell renderer so the rule lives\n * in one tested place — a value this screen has no word for is printed as it arrived.\n */\n return {\n total,\n records: items.map((item: any) => ({\n ...item,\n statusLabel: applicationStatusLabel(item.status, (key: string) => i18next.t(key))\n }))\n }\n }\n}\n"]}
@@ -3,9 +3,20 @@ import gql from 'graphql-tag';
3
3
  import { css, html } from 'lit';
4
4
  import { customElement, property, query, state } from 'lit/decorators.js';
5
5
  import { client } from '@operato/graphql';
6
+ import { i18next, localize } from '@operato/i18n';
6
7
  import { navigate, PageView } from '@operato/shell';
7
- let ApplicationRegister = class ApplicationRegister extends PageView {
8
+ import { FormFieldStyles } from '@operato/styles';
9
+ import { filledValues, missingRequired } from '../../utils/form-values.js';
10
+ let ApplicationRegister = class ApplicationRegister extends localize(i18next)(PageView) {
11
+ constructor() {
12
+ super(...arguments);
13
+ /** Required fields the person has not filled in yet. Empty until they press register. */
14
+ this.missing = [];
15
+ /** The mutation was refused. The link already said why; this says the press landed. */
16
+ this.failed = false;
17
+ }
8
18
  static { this.styles = [
19
+ FormFieldStyles,
9
20
  css `
10
21
  :host { background-color: var(--md-sys-color-background);
11
22
  padding: var(--spacing-large);
@@ -22,6 +33,15 @@ let ApplicationRegister = class ApplicationRegister extends PageView {
22
33
  font: var(--page-description-font);
23
34
  color: var(--page-description-color);
24
35
  }
36
+ [field-error],
37
+ [form-error] { color: var(--md-sys-color-error);
38
+ font: var(--input-hint-font, inherit);
39
+ margin-top: var(--spacing-tiny, 2px);
40
+ }
41
+
42
+ input[invalid] { border-color: var(--md-sys-color-error);
43
+ }
44
+
25
45
  [icon] { position: absolute;
26
46
  top: 10px;
27
47
  right: 10px;
@@ -92,11 +112,8 @@ let ApplicationRegister = class ApplicationRegister extends PageView {
92
112
  render() {
93
113
  return html `
94
114
  <img src="/assets/images/image-api.png" pagedeco />
95
- <h2>Register new application</h2>
96
- <p page-description>
97
- You can register new application here.<br />
98
- What type of app are you building? Choose the app type that best suits the audience you’re building for.
99
- </p>
115
+ <h2>${i18next.t('text.register new application')}</h2>
116
+ <p page-description>${i18next.t('text.an application signs in on its own behalf')}</p>
100
117
 
101
118
  ${this._icon
102
119
  ? html `
@@ -110,36 +127,44 @@ let ApplicationRegister = class ApplicationRegister extends PageView {
110
127
  <form>
111
128
  <div field-2column>
112
129
  <div field grid-span>
113
- <label for="name">name</label>
114
- <input id="name" type="text" name="name" />
130
+ <label for="name">${i18next.t('field.name')}</label>
131
+ <input id="name" type="text" name="name" required ?invalid=${this.missing.includes('name')} />
132
+ ${this.missing.includes('name')
133
+ ? html `<div field-error>${i18next.t('text.this field is required')}</div>`
134
+ : ''}
115
135
  </div>
116
136
 
117
137
  <div field grid-span>
118
- <label for="description">description</label>
138
+ <label for="description">${i18next.t('field.description')}</label>
119
139
  <input id="description" type="text" name="description" />
120
140
  </div>
121
141
 
122
142
  <div field grid-span>
123
- <label for="app-url">application url</label>
124
- <input id="app-url" type="text" name="url" />
143
+ <label for="app-url">${i18next.t('field.application url')}</label>
144
+ <input id="app-url" type="url" name="url" />
125
145
  </div>
126
146
 
127
147
  <div field grid-span>
128
- <label for="redirect-url">redirectUrl</label>
129
- <input id="redirect-url" type="text" name="redirectUrl" />
148
+ <label for="redirect-url">${i18next.t('field.redirect url')}</label>
149
+ <input id="redirect-url" type="url" name="redirectUrl" />
130
150
  </div>
131
151
 
132
152
  <div field grid-span>
133
- <label for="icon">application icon</label>
134
- <input id="icon" type="text" name="icon" @change=${e => (this._icon = e.target.value)} />
153
+ <label for="icon">${i18next.t('field.application icon')}</label>
154
+ <input id="icon" type="url" name="icon" @change=${e => (this._icon = e.target.value)} />
135
155
  </div>
136
156
 
137
157
  <div field grid-span>
138
- <label for="email">contact email</label>
139
- <input id="email" type="text" name="email" />
158
+ <!-- type=email so the browser checks the shape the server's scalar will check. -->
159
+ <label for="email">${i18next.t('field.contact email')}</label>
160
+ <input id="email" type="email" name="email" />
140
161
  </div>
141
162
 
142
- <md-elevated-button grid-span @click=${this.createApplication.bind(this)}>register</md-elevated-button>
163
+ ${this.failed ? html `<div form-error grid-span>${i18next.t('text.could not register')}</div>` : ''}
164
+
165
+ <button grid-span type="submit" @click=${this.createApplication.bind(this)}>
166
+ ${i18next.t('button.register')}
167
+ </button>
143
168
  </div>
144
169
  </form>
145
170
  </div>
@@ -148,10 +173,27 @@ let ApplicationRegister = class ApplicationRegister extends PageView {
148
173
  async createApplication(e) {
149
174
  e.preventDefault();
150
175
  const formData = new FormData(this.form);
151
- const application = Array.from(formData.entries()).reduce((application, [key, value]) => {
152
- application[key] = value;
153
- return application;
154
- }, {});
176
+ /*
177
+ `name` is the only field NewApplication declares non-null. The list comes from the
178
+ schema rather than from taste — requiring more here would invent a rule the product
179
+ does not have, and the person would be stopped by this screen alone.
180
+ */
181
+ const missing = missingRequired(formData, ['name']);
182
+ if (missing.length) {
183
+ this.missing = missing;
184
+ return;
185
+ }
186
+ this.missing = [];
187
+ /*
188
+ This used to send every input, including the untouched ones, each as an empty string.
189
+ `NewApplication.email` is a GraphQLEmailAddress and rejects '' — so registering without
190
+ a contact email failed, on a field the schema marks optional, and the screen printed
191
+ "creation fail." to the console where nobody would see it.
192
+
193
+ `filledValues` leaves an empty field out instead, which is what optional means on the
194
+ wire.
195
+ */
196
+ const application = filledValues(formData);
155
197
  const response = await client.mutate({ mutation: gql `
156
198
  mutation ($application: NewApplication!) { createApplication(application: $application) { id
157
199
  }
@@ -161,12 +203,16 @@ let ApplicationRegister = class ApplicationRegister extends PageView {
161
203
  }
162
204
  });
163
205
  if (response.errors) {
164
- console.log('creation fail.');
165
- }
166
- else {
167
- const id = response.data.createApplication.id;
168
- navigate(`application/${id}`);
206
+ /*
207
+ The shared client link already raises a notify event carrying the server's reason, so
208
+ the person sees what went wrong. What was missing is that the screen stayed silent
209
+ and looked idle — the form is still filled in, and nothing said the press had landed.
210
+ */
211
+ this.failed = true;
212
+ return;
169
213
  }
214
+ this.failed = false;
215
+ navigate(`application/${response.data.createApplication.id}`);
170
216
  }
171
217
  pageUpdated(changes, lifecycle, before) {
172
218
  if (this.active) {
@@ -186,6 +232,14 @@ __decorate([
186
232
  state(),
187
233
  __metadata("design:type", String)
188
234
  ], ApplicationRegister.prototype, "_icon", void 0);
235
+ __decorate([
236
+ state(),
237
+ __metadata("design:type", Array)
238
+ ], ApplicationRegister.prototype, "missing", void 0);
239
+ __decorate([
240
+ state(),
241
+ __metadata("design:type", Boolean)
242
+ ], ApplicationRegister.prototype, "failed", void 0);
189
243
  ApplicationRegister = __decorate([
190
244
  customElement('application-register')
191
245
  ], ApplicationRegister);
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../../../client/pages/application/register.ts"],"names":[],"mappings":";AAAA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGnD,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,QAAQ;aAAY,WAAM,GAAG;QAC3D,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA4EF;KACF,AA9EyD,CA8EzD;IAQD,IAAI,OAAO;QAAS,OAAO,EAAQ,KAAK,EAAE,0BAA0B;YAChE,IAAI,EAAE,kBAAkB;SAC5B,CAAA;IACD,CAAC;IAEA,MAAM;QAAS,OAAO,IAAI,CAAA;;;;;;;;QAQpB,IAAI,CAAC,KAAK;YACV,CAAC,CAAC,IAAI,CAAA;;yBAEW,IAAI,CAAC,KAAK;;WAExB;YACH,CAAC,CAAC,IAAI,CAAA,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;iEA2BiD,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;;;;;;;mDAQhD,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;;;;KAI/E,CAAA;IACJ,CAAC;IAEA,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAAQ,CAAC,CAAC,cAAc,EAAE,CAAA;QAEjD,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAExC,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAAS,WAAW,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;YACvH,OAAO,WAAW,CAAA;QACvB,CAAC,EAAE,EAAE,CAAC,CAAA;QAEH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,EAAQ,QAAQ,EAAE,GAAG,CAAA;;;;OAIvD;YACD,SAAS,EAAE,EAAU,WAAW;aACpC;SACA,CAAC,CAAA;QAEC,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;QAC7D,CAAC;aAAM,CAAC;YAAO,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,EAAE,CAAA;YACvD,QAAQ,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;QAClC,CAAC;IACD,CAAC;IAEA,WAAW,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM;QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAA;QACzF,CAAC;IACD,CAAC;;AA5F4B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;wDAAiB;AAE7B;IAAd,KAAK,CAAC,MAAM,CAAC;8BAAQ,eAAe;iDAAA;AAE5B;IAAR,KAAK,EAAE;;kDAAe;AApFnB,mBAAmB;IADxB,aAAa,CAAC,sBAAsB,CAAC;GAChC,mBAAmB,CA6KxB","sourcesContent":["import gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { navigate, PageView } from '@operato/shell'\n\n@customElement('application-register')\nclass ApplicationRegister extends PageView { static styles = [\n css`\n :host { background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n\n position: relative;\n\n overflow: auto;\n }\n h2 { margin: var(--title-margin);\n font: var(--title-font);\n color: var(--title-text-color);\n }\n [page-description] { margin: var(--page-description-margin);\n font: var(--page-description-font);\n color: var(--page-description-color);\n }\n [icon] { position: absolute;\n top: 10px;\n right: 10px;\n\n max-width: 80px;\n }\n [icon] img { max-width: 100%;\n max-height: 100%;\n }\n\n img[pagedeco] { width: 150px;\n margin: 0 var(--spacing-large);\n float: left;\n position: relative;\n top: -20px;\n }\n\n [form-container] { background-color: var(--md-sys-color-surface);\n padding: var(--spacing-large);\n border-radius: var(--border-radius);\n box-shadow: var(--box-shadow);\n max-width: var(--input-container-max-width);\n position: relative;\n top: -50px;\n clear: both;\n }\n label { font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n font: var(--input-font);\n\n flex: 1;\n }\n select:focus,\n input:focus { outline: none;\n }\n [field-2column] { display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: 15px;\n }\n [field] { display: flex;\n flex-direction: column;\n }\n [grid-span] { grid-column: span 2;\n }\n @media screen and (max-width: 480px) { img[pagedeco] { display: none;\n }\n [form-container] { position: initial;\n }\n [field] { grid-column: span 2;\n }\n }\n `\n ]\n\n @property({ type: Object }) application: any\n\n @query('form') form!: HTMLFormElement\n\n @state() _icon?: string\n\n get context() { return { title: `application registration`,\n help: 'auth/application'\n }\n }\n\n render() { return html`\n <img src=\"/assets/images/image-api.png\" pagedeco />\n <h2>Register new application</h2>\n <p page-description>\n You can register new application here.<br />\n What type of app are you building? Choose the app type that best suits the audience you’re building for.\n </p>\n\n ${this._icon\n ? html`\n <div icon>\n <img src=${this._icon} />\n </div>\n `\n : html``}\n\n <div form-container>\n <form>\n <div field-2column>\n <div field grid-span>\n <label for=\"name\">name</label>\n <input id=\"name\" type=\"text\" name=\"name\" />\n </div>\n\n <div field grid-span>\n <label for=\"description\">description</label>\n <input id=\"description\" type=\"text\" name=\"description\" />\n </div>\n\n <div field grid-span>\n <label for=\"app-url\">application url</label>\n <input id=\"app-url\" type=\"text\" name=\"url\" />\n </div>\n\n <div field grid-span>\n <label for=\"redirect-url\">redirectUrl</label>\n <input id=\"redirect-url\" type=\"text\" name=\"redirectUrl\" />\n </div>\n\n <div field grid-span>\n <label for=\"icon\">application icon</label>\n <input id=\"icon\" type=\"text\" name=\"icon\" @change=${e => (this._icon = e.target.value)} />\n </div>\n\n <div field grid-span>\n <label for=\"email\">contact email</label>\n <input id=\"email\" type=\"text\" name=\"email\" />\n </div>\n\n <md-elevated-button grid-span @click=${this.createApplication.bind(this)}>register</md-elevated-button>\n </div>\n </form>\n </div>\n `\n }\n\n async createApplication(e) { e.preventDefault()\n\n const formData = new FormData(this.form)\n\n const application = Array.from(formData.entries()).reduce((application, [key, value]) => { application[key] = value\n return application\n }, {})\n\n const response = await client.mutate({ mutation: gql`\n mutation ($application: NewApplication!) { createApplication(application: $application) { id\n }\n }\n `,\n variables: { application\n }\n })\n\n if (response.errors) { console.log('creation fail.')\n } else { const id = response.data.createApplication.id\n navigate(`application/${id}`)\n }\n }\n\n pageUpdated(changes, lifecycle, before) { if (this.active) { this.form.reset()\n }\n }\n}\n"]}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../../client/pages/application/register.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { navigate, PageView } from '@operato/shell'\nimport { FormFieldStyles } from '@operato/styles'\n\nimport { filledValues, missingRequired } from '../../utils/form-values.js'\n\n@customElement('application-register')\nclass ApplicationRegister extends localize(i18next)(PageView) { static styles = [\n FormFieldStyles,\n css`\n :host { background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n\n position: relative;\n\n overflow: auto;\n }\n h2 { margin: var(--title-margin);\n font: var(--title-font);\n color: var(--title-text-color);\n }\n [page-description] { margin: var(--page-description-margin);\n font: var(--page-description-font);\n color: var(--page-description-color);\n }\n [field-error],\n [form-error] { color: var(--md-sys-color-error);\n font: var(--input-hint-font, inherit);\n margin-top: var(--spacing-tiny, 2px);\n }\n\n input[invalid] { border-color: var(--md-sys-color-error);\n }\n\n [icon] { position: absolute;\n top: 10px;\n right: 10px;\n\n max-width: 80px;\n }\n [icon] img { max-width: 100%;\n max-height: 100%;\n }\n\n img[pagedeco] { width: 150px;\n margin: 0 var(--spacing-large);\n float: left;\n position: relative;\n top: -20px;\n }\n\n [form-container] { background-color: var(--md-sys-color-surface);\n padding: var(--spacing-large);\n border-radius: var(--border-radius);\n box-shadow: var(--box-shadow);\n max-width: var(--input-container-max-width);\n position: relative;\n top: -50px;\n clear: both;\n }\n label { font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n font: var(--input-font);\n\n flex: 1;\n }\n select:focus,\n input:focus { outline: none;\n }\n [field-2column] { display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: 15px;\n }\n [field] { display: flex;\n flex-direction: column;\n }\n [grid-span] { grid-column: span 2;\n }\n @media screen and (max-width: 480px) { img[pagedeco] { display: none;\n }\n [form-container] { position: initial;\n }\n [field] { grid-column: span 2;\n }\n }\n `\n ]\n\n @property({ type: Object }) application: any\n\n @query('form') form!: HTMLFormElement\n\n @state() _icon?: string\n\n /** Required fields the person has not filled in yet. Empty until they press register. */\n @state() missing: string[] = []\n\n /** The mutation was refused. The link already said why; this says the press landed. */\n @state() failed: boolean = false\n\n get context() { return { title: `application registration`,\n help: 'auth/application'\n }\n }\n\n render() { return html`\n <img src=\"/assets/images/image-api.png\" pagedeco />\n <h2>${i18next.t('text.register new application')}</h2>\n <p page-description>${i18next.t('text.an application signs in on its own behalf')}</p>\n\n ${this._icon\n ? html`\n <div icon>\n <img src=${this._icon} />\n </div>\n `\n : html``}\n\n <div form-container>\n <form>\n <div field-2column>\n <div field grid-span>\n <label for=\"name\">${i18next.t('field.name')}</label>\n <input id=\"name\" type=\"text\" name=\"name\" required ?invalid=${this.missing.includes('name')} />\n ${this.missing.includes('name')\n ? html`<div field-error>${i18next.t('text.this field is required')}</div>`\n : ''}\n </div>\n\n <div field grid-span>\n <label for=\"description\">${i18next.t('field.description')}</label>\n <input id=\"description\" type=\"text\" name=\"description\" />\n </div>\n\n <div field grid-span>\n <label for=\"app-url\">${i18next.t('field.application url')}</label>\n <input id=\"app-url\" type=\"url\" name=\"url\" />\n </div>\n\n <div field grid-span>\n <label for=\"redirect-url\">${i18next.t('field.redirect url')}</label>\n <input id=\"redirect-url\" type=\"url\" name=\"redirectUrl\" />\n </div>\n\n <div field grid-span>\n <label for=\"icon\">${i18next.t('field.application icon')}</label>\n <input id=\"icon\" type=\"url\" name=\"icon\" @change=${e => (this._icon = e.target.value)} />\n </div>\n\n <div field grid-span>\n <!-- type=email so the browser checks the shape the server's scalar will check. -->\n <label for=\"email\">${i18next.t('field.contact email')}</label>\n <input id=\"email\" type=\"email\" name=\"email\" />\n </div>\n\n ${this.failed ? html`<div form-error grid-span>${i18next.t('text.could not register')}</div>` : ''}\n\n <button grid-span type=\"submit\" @click=${this.createApplication.bind(this)}>\n ${i18next.t('button.register')}\n </button>\n </div>\n </form>\n </div>\n `\n }\n\n async createApplication(e) { e.preventDefault()\n\n const formData = new FormData(this.form)\n\n /*\n `name` is the only field NewApplication declares non-null. The list comes from the\n schema rather than from taste — requiring more here would invent a rule the product\n does not have, and the person would be stopped by this screen alone.\n */\n const missing = missingRequired(formData, ['name'])\n\n if (missing.length) {\n this.missing = missing\n return\n }\n\n this.missing = []\n\n /*\n This used to send every input, including the untouched ones, each as an empty string.\n `NewApplication.email` is a GraphQLEmailAddress and rejects '' — so registering without\n a contact email failed, on a field the schema marks optional, and the screen printed\n \"creation fail.\" to the console where nobody would see it.\n\n `filledValues` leaves an empty field out instead, which is what optional means on the\n wire.\n */\n const application = filledValues(formData)\n\n const response = await client.mutate({ mutation: gql`\n mutation ($application: NewApplication!) { createApplication(application: $application) { id\n }\n }\n `,\n variables: { application\n }\n })\n\n if (response.errors) {\n /*\n The shared client link already raises a notify event carrying the server's reason, so\n the person sees what went wrong. What was missing is that the screen stayed silent\n and looked idle — the form is still filled in, and nothing said the press had landed.\n */\n this.failed = true\n return\n }\n\n this.failed = false\n navigate(`application/${response.data.createApplication.id}`)\n }\n\n pageUpdated(changes, lifecycle, before) { if (this.active) { this.form.reset()\n }\n }\n}\n"]}
@@ -13,6 +13,8 @@ export declare class AuthProviderManagementPage extends AuthProviderManagementPa
13
13
  static styles: import("lit").CSSResult[];
14
14
  gristConfig: any;
15
15
  mode: 'CARD' | 'GRID' | 'LIST';
16
+ /** The last fetch did not answer. Distinct from answering with nothing. */
17
+ failed: boolean;
16
18
  authProviderTypes: {
17
19
  type: string;
18
20
  description?: string;
@@ -47,8 +49,8 @@ export declare class AuthProviderManagementPage extends AuthProviderManagementPa
47
49
  render(): import("lit-html").TemplateResult<1>;
48
50
  pageInitialized(lifecycle: any): Promise<void>;
49
51
  fetchHandler({ page, limit, sortings, filters }: FetchOption): Promise<{
50
- total: any;
51
- records: any;
52
+ total: number;
53
+ records: any[];
52
54
  }>;
53
55
  private deleteAuthProvider;
54
56
  private updateAuthProvider;