@operato/app 0.4.2 → 1.0.0-alpha.2

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.
@@ -1,14 +1,6 @@
1
1
  import '@operato/data-grist'
2
2
 
3
- import {
4
- DataGrist,
5
- FetchOption,
6
- GristConfig,
7
- GristData,
8
- GristRecord,
9
- ZERO_CONFIG,
10
- ZERO_DATA
11
- } from '@operato/data-grist'
3
+ import { DataGrist, FetchOption, GristData, GristRecord, ZERO_DATA } from '@operato/data-grist'
12
4
  import { LitElement, css, html } from 'lit'
13
5
  import { customElement, property, query } from 'lit/decorators.js'
14
6
 
@@ -68,7 +60,7 @@ export class IdSelector extends LitElement {
68
60
  .fetchHandler=${this.fetchHandler.bind(this)}
69
61
  .selectedRecords=${this.selectedRecords}
70
62
  >
71
- <div id="filters" slot="headless">
63
+ <div id="filters" slot="headroom">
72
64
  <ox-filters-form></ox-filters-form>
73
65
  </div>
74
66
  </ox-grist>
@@ -1,9 +1,11 @@
1
1
  import { registerEditor, registerRenderer } from '@operato/data-grist'
2
2
 
3
+ import { CodeEditor } from './code-editor'
3
4
  import { IdInput } from './id-input.js'
4
5
  import { IdRenderer } from './id-renderer.js'
5
6
 
6
7
  /* register grist renderer/editor for id */
7
8
  registerEditor('id', IdInput)
9
+ registerEditor('code', CodeEditor)
8
10
 
9
11
  registerRenderer('id', IdRenderer)
@@ -0,0 +1,154 @@
1
+ import '@material/mwc-icon'
2
+ import '../../elements/object-selector'
3
+
4
+ import { ColumnConfig, GristData, GristRecord, ZERO_COLUMN, ZERO_CONFIG, ZERO_RECORD } from '@operato/data-grist'
5
+ import { LitElement, TemplateResult, css, html } from 'lit'
6
+ import { PopupHandle, openPopup } from '@operato/layout'
7
+ import { customElement, property } from 'lit/decorators.js'
8
+
9
+ import { i18next } from '@operato/i18n'
10
+
11
+ @customElement('ox-object-editor')
12
+ export class ObjectEditor extends LitElement {
13
+ static styles = css`
14
+ :host {
15
+ display: flex;
16
+ flex-flow: row nowrap;
17
+
18
+ padding: 7px 0px;
19
+ box-sizing: border-box;
20
+
21
+ width: 100%;
22
+ height: 100%;
23
+
24
+ border: 0;
25
+ background-color: transparent;
26
+
27
+ font: var(--grist-object-editor-font);
28
+ color: var(--grist-object-editor-color);
29
+ justify-content: inherit;
30
+ }
31
+
32
+ span {
33
+ display: flex;
34
+ flex: auto;
35
+
36
+ justify-content: inherit;
37
+ }
38
+
39
+ mwc-icon {
40
+ width: 20px;
41
+ font-size: 1.5em;
42
+ margin-left: auto;
43
+ }
44
+ `
45
+
46
+ @property({ type: Object }) value: { [prop: string]: any } = {}
47
+ @property({ type: Object }) column: ColumnConfig = ZERO_COLUMN
48
+ @property({ type: Object }) record: GristRecord = ZERO_RECORD
49
+ @property({ type: Number }) row?: number
50
+
51
+ private popup?: PopupHandle
52
+ private template?: TemplateResult
53
+
54
+ render() {
55
+ var value = this.value || {}
56
+
57
+ var { nameField = 'name', descriptionField = 'description' } = this.column.record.options || {}
58
+ var name, description
59
+ if (typeof nameField === 'function') {
60
+ name = nameField(value)
61
+ } else {
62
+ name = value[nameField]
63
+ }
64
+
65
+ if (typeof descriptionField === 'function') {
66
+ description = descriptionField(value)
67
+ } else {
68
+ description = value[descriptionField] && `(${value[descriptionField]})`
69
+ }
70
+
71
+ return html`
72
+ ${!value ? html`` : html` <span>${name || ''}${description || ''}</span> `}
73
+ <mwc-icon>arrow_drop_down</mwc-icon>
74
+ `
75
+ }
76
+
77
+ async firstUpdated() {
78
+ this.value = this.record[this.column.name]
79
+ this.template = ((this.column.record || {}).options || {}).template
80
+
81
+ await this.updateComplete
82
+
83
+ this.renderRoot.addEventListener('click', e => {
84
+ e.stopPropagation()
85
+
86
+ this.openSelector()
87
+ })
88
+ }
89
+
90
+ openSelector() {
91
+ if (this.popup) {
92
+ delete this.popup
93
+ }
94
+
95
+ const confirmCallback = (selected: { [field: string]: any }) => {
96
+ var { idField = 'id', nameField = 'name', descriptionField = 'description' } = this.column.record.options || {}
97
+
98
+ this.dispatchEvent(
99
+ new CustomEvent('field-change', {
100
+ bubbles: true,
101
+ composed: true,
102
+ detail: {
103
+ before: this.value,
104
+ after: {
105
+ ...(this.column.record.options.select || [])
106
+ .map((field: any) => field.name)
107
+ .reduce((obj: { [field: string]: any }, fieldName: string) => {
108
+ return (obj = {
109
+ ...obj,
110
+ [fieldName]: selected[fieldName]
111
+ })
112
+ }, {} as { [field: string]: any }),
113
+ [idField]: selected[idField],
114
+ [nameField]: selected[nameField],
115
+ [descriptionField]: selected[descriptionField]
116
+ },
117
+ record: this.record,
118
+ column: this.column,
119
+ row: this.row
120
+ }
121
+ })
122
+ )
123
+ }
124
+
125
+ var value = this.value || {}
126
+ var valueField: string | ((item: any) => any) = this.column.record.options.valueField || 'id'
127
+ var actualValue
128
+ if (typeof valueField === 'function') {
129
+ actualValue = valueField(value)
130
+ } else {
131
+ actualValue = value[valueField]
132
+ }
133
+
134
+ var template =
135
+ this.template ||
136
+ html`
137
+ <object-selector
138
+ .value=${actualValue}
139
+ .confirmCallback=${confirmCallback.bind(this)}
140
+ .queryName=${this.column.record.options.queryName}
141
+ .select=${this.column.record.options.select}
142
+ .list=${this.column.record.options.list}
143
+ .basicArgs=${this.column.record.options.basicArgs}
144
+ .valueField=${this.column.record.options.valueField}
145
+ ></object-selector>
146
+ `
147
+
148
+ this.popup = openPopup(template, {
149
+ backdrop: true,
150
+ size: 'large',
151
+ title: i18next.t('title.select_item')
152
+ })
153
+ }
154
+ }
@@ -0,0 +1,346 @@
1
+ import '@operato/data-grist'
2
+ import '@operato/form/ox-search-form.js'
3
+
4
+ import {
5
+ ColumnConfig,
6
+ DataGrist,
7
+ FetchHandler,
8
+ FetchOption,
9
+ GristData,
10
+ GristEventHandler,
11
+ GristRecord,
12
+ ZERO_DATA
13
+ } from '@operato/data-grist'
14
+ import { LitElement, css, html } from 'lit'
15
+ import { MultiColumnFormStyles, SearchForm } from '@operato/form'
16
+ import { buildArgs, client, gqlContext } from '@operato/graphql'
17
+ import { customElement, property, query } from 'lit/decorators.js'
18
+
19
+ import gql from 'graphql-tag'
20
+ import { i18next } from '@operato/i18n'
21
+ import { isMobileDevice } from '@operato/utils'
22
+
23
+ @customElement('ox-object-selector')
24
+ export class ObjectSelector extends LitElement {
25
+ static styles = [
26
+ MultiColumnFormStyles,
27
+ css`
28
+ :host {
29
+ display: flex;
30
+ flex-direction: column;
31
+
32
+ background-color: #fff;
33
+ }
34
+
35
+ data-grist {
36
+ flex: 1;
37
+ }
38
+
39
+ .button-container {
40
+ display: flex;
41
+ margin-left: auto;
42
+ }
43
+
44
+ form {
45
+ position: relative;
46
+ }
47
+
48
+ [search] {
49
+ position: absolute;
50
+ right: 0;
51
+ }
52
+ `
53
+ ]
54
+
55
+ @property({ type: String }) value?: string
56
+ @property({ type: Object }) config: any
57
+ @property({ type: Object }) data: GristData = ZERO_DATA
58
+ @property({ type: String }) queryName!: string
59
+ @property({ type: Object }) basicArgs: any
60
+ @property({ type: Object }) confirmCallback?: (record: GristRecord | undefined) => void
61
+ @property({ type: Array }) selectedRecords: GristRecord[] = []
62
+
63
+ @property({ type: Array }) searchFields: any
64
+ @property({ type: Array }) select: {
65
+ type: string
66
+ name: string
67
+ header: string
68
+ subFields?: string[]
69
+ hidden: boolean
70
+ ignoreCondition: boolean
71
+ queryName: string
72
+ width: number
73
+ }[] = []
74
+ @property({ type: Object }) list: any
75
+ @property({ type: String }) valueField: string | ((item: any) => any) = 'id'
76
+
77
+ @query('ox-grist') grist!: DataGrist
78
+ @query('ox-search-form') searchForm!: SearchForm
79
+
80
+ render() {
81
+ return html`
82
+ <ox-search-form
83
+ id="search-form"
84
+ @keypress=${(e: KeyboardEvent) => {
85
+ if (e.keyCode === 13) {
86
+ this.grist.fetch()
87
+ }
88
+ }}
89
+ @submit=${(e: SubmitEvent) => this.grist.fetch()}
90
+ .fields=${this.searchFields}
91
+ ></ox-search-form>
92
+
93
+ <ox-grist
94
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
95
+ .config=${this.config}
96
+ .data=${this.data}
97
+ .fetchHandler=${this.fetchHandler.bind(this)}
98
+ .selectedRecords=${this.selectedRecords}
99
+ ></ox-grist>
100
+
101
+ <div class="button-container">
102
+ <mwc-button @click=${this.oncancel.bind(this)}>${i18next.t('button.cancel')}</mwc-button>
103
+ <mwc-button @click=${this.onconfirm.bind(this)}>${i18next.t('button.confirm')}</mwc-button>
104
+ </div>
105
+ `
106
+ }
107
+
108
+ oncancel() {
109
+ history.back()
110
+ }
111
+
112
+ onconfirm() {
113
+ this.confirmCallback && this.confirmCallback(this.selected)
114
+ history.back()
115
+ }
116
+
117
+ fetchHandler: FetchHandler = async ({ filters, page, limit, sorters = [] }) => {
118
+ const response = await client.query({
119
+ query: gql`
120
+ query {
121
+ fetch: ${this.queryName} (${buildArgs(await this._buildConditions({ filters, page, limit, sorters }))}) {
122
+ ${this.getSelectFields()}
123
+ }
124
+ }
125
+ `,
126
+ context: gqlContext()
127
+ })
128
+
129
+ if (!response.errors) {
130
+ const records = response.data.fetch.items.map((item: any) => {
131
+ let rowValue
132
+
133
+ if (this.valueField && typeof this.valueField === 'function') {
134
+ rowValue = this.valueField(item)
135
+ } else if (this.valueField) {
136
+ rowValue = item[this.valueField]
137
+ } else {
138
+ rowValue = item.id
139
+ }
140
+
141
+ if (this.value && this.value === rowValue) {
142
+ this.selectedRecords = [item]
143
+ item['__selected__'] = true
144
+ }
145
+
146
+ return item
147
+ })
148
+ const total = response.data.fetch.total
149
+
150
+ return {
151
+ records,
152
+ total,
153
+ limit,
154
+ page
155
+ }
156
+ }
157
+ }
158
+
159
+ async firstUpdated() {
160
+ this.config = {
161
+ columns: [
162
+ {
163
+ type: 'gutter',
164
+ gutterName: 'sequence'
165
+ },
166
+ {
167
+ type: 'gutter',
168
+ gutterName: 'row-selector',
169
+ multiple: false
170
+ }
171
+ ],
172
+ rows: {
173
+ selectable: {
174
+ multiple: false
175
+ },
176
+ handlers: {
177
+ click: 'select-row',
178
+ dblclick: ((columns, data, column, record, rowIndex, field) => {
179
+ this.onconfirm()
180
+ }) as GristEventHandler
181
+ },
182
+ appendable: false
183
+ }
184
+ }
185
+
186
+ if (this.select && this.select.length > 0) {
187
+ let _searchFields = this.select.filter(selectField => !selectField.hidden && !selectField.ignoreCondition)
188
+ if (this.list && this.list.fields && this.list.fields.length > 0) {
189
+ _searchFields = _searchFields.filter(searchField => this.list.fields.indexOf(searchField.name) >= 0)
190
+ } else {
191
+ _searchFields = _searchFields.slice(0, 4)
192
+ }
193
+
194
+ this.searchFields = _searchFields.map(selectField => {
195
+ const fieldType = (selectField.type && selectField.type.toLowerCase()) || 'string'
196
+ const numberTypes = ['integer', 'float']
197
+ return {
198
+ label: selectField.header || i18next.t(`field.${selectField.name}`),
199
+ name: selectField.name,
200
+ type:
201
+ fieldType === 'string'
202
+ ? 'text'
203
+ : numberTypes.indexOf(fieldType) >= 0
204
+ ? 'number'
205
+ : fieldType === 'boolean'
206
+ ? 'checkbox'
207
+ : fieldType,
208
+ queryName: selectField.queryName,
209
+ props:
210
+ fieldType === 'string'
211
+ ? { searchOper: 'i_like' }
212
+ : fieldType === 'object'
213
+ ? { searchOper: 'in' }
214
+ : { searchOper: 'eq' },
215
+ attrs: fieldType === 'boolean' ? ['indeterminated'] : []
216
+ }
217
+ })
218
+ this.config = {
219
+ ...this.config,
220
+ columns: [
221
+ ...this.config.columns,
222
+ ...this.select.map(selectField => {
223
+ return {
224
+ ...selectField,
225
+ type: selectField.type || 'string',
226
+ width: selectField.width || 160,
227
+ header: selectField.header || i18next.t(`field.${selectField.name}`)
228
+ }
229
+ })
230
+ ]
231
+ }
232
+ } else {
233
+ this.searchFields = [
234
+ {
235
+ label: i18next.t('field.name'),
236
+ name: 'name',
237
+ type: 'text',
238
+ props: { searchOper: 'i_like' }
239
+ },
240
+ {
241
+ label: i18next.t('field.description'),
242
+ name: 'description',
243
+ type: 'text',
244
+ props: { searchOper: 'i_like' }
245
+ }
246
+ ]
247
+
248
+ this.config = {
249
+ ...this.config,
250
+ columns: [
251
+ ...this.config.columns,
252
+ {
253
+ type: 'string',
254
+ name: 'id',
255
+ header: i18next.t('field.id'),
256
+ hidden: true
257
+ },
258
+ {
259
+ type: 'string',
260
+ name: 'name',
261
+ header: i18next.t('field.name'),
262
+ record: {
263
+ align: 'left'
264
+ },
265
+ sortable: true,
266
+ width: 160
267
+ },
268
+ {
269
+ type: 'string',
270
+ name: 'description',
271
+ header: i18next.t('field.description'),
272
+ record: {
273
+ align: 'left'
274
+ },
275
+ sortable: true,
276
+ width: 300
277
+ }
278
+ ]
279
+ }
280
+ }
281
+
282
+ this.config = {
283
+ ...this.config,
284
+ list: {
285
+ ...this.list,
286
+ fields:
287
+ this.list && this.list.fields && this.list.fields.length > 0
288
+ ? this.list.fields
289
+ : (this.config.columns as ColumnConfig[])
290
+ .filter(column => column.type !== 'gutter')
291
+ .slice(0, 3)
292
+ .map(column => column.name)
293
+ }
294
+ }
295
+
296
+ await this.updateComplete
297
+ this.grist && this.grist.focus()
298
+ }
299
+
300
+ getSelectFields() {
301
+ if (this.select && this.select.length > 0) {
302
+ return `items {
303
+ ${this.select.map(selectField => {
304
+ return selectField.type === 'object'
305
+ ? `${selectField.name} { ${
306
+ selectField.subFields && selectField.subFields.length > 0
307
+ ? selectField.subFields.join(' ')
308
+ : `id name description`
309
+ } }`
310
+ : `${selectField.name}`
311
+ })}
312
+ }
313
+ total`
314
+ } else {
315
+ return `
316
+ items {
317
+ id
318
+ name
319
+ description
320
+ }
321
+ total
322
+ `
323
+ }
324
+ }
325
+
326
+ async _buildConditions({ filters, page, limit, sorters }: FetchOption) {
327
+ const queryConditions = {
328
+ filters: [],
329
+ ...this.basicArgs
330
+ }
331
+
332
+ queryConditions.filters = [...queryConditions.filters, ...(await this.searchForm.getQueryFilters())]
333
+ queryConditions.pagination = { page, limit }
334
+ queryConditions.sortings = sorters
335
+
336
+ return queryConditions
337
+ }
338
+
339
+ get selected() {
340
+ var grist = this.renderRoot.querySelector('ox-grist') as DataGrist
341
+
342
+ var selected = grist.selected
343
+
344
+ return selected && selected.length > 0 ? selected[0] : undefined
345
+ }
346
+ }