@operato/app 8.0.0-beta.0 → 8.0.0-beta.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +14 -14
  3. package/.editorconfig +0 -29
  4. package/.storybook/main.js +0 -3
  5. package/.storybook/server.mjs +0 -8
  6. package/demo/data-grist-test.html +0 -473
  7. package/demo/index.html +0 -35
  8. package/src/filter-renderer/filter-resource-select.ts +0 -133
  9. package/src/filter-renderer/index.ts +0 -6
  10. package/src/filters-form/filter-resource-code.ts +0 -11
  11. package/src/filters-form/filter-resource-object.ts +0 -11
  12. package/src/filters-form/filter-resource-select.ts +0 -130
  13. package/src/filters-form/index.ts +0 -14
  14. package/src/filters-form/ox-filter-resource-code.ts +0 -154
  15. package/src/filters-form/ox-filter-resource-object.ts +0 -224
  16. package/src/grist-editor/index.ts +0 -26
  17. package/src/grist-editor/ox-grist-editor-code.ts +0 -64
  18. package/src/grist-editor/ox-grist-editor-json.ts +0 -64
  19. package/src/grist-editor/ox-grist-editor-privilege.ts +0 -58
  20. package/src/grist-editor/ox-grist-editor-resource-code.ts +0 -30
  21. package/src/grist-editor/ox-grist-editor-resource-id.ts +0 -88
  22. package/src/grist-editor/ox-grist-editor-resource-object-legacy.ts +0 -131
  23. package/src/grist-editor/ox-grist-editor-resource-object.ts +0 -156
  24. package/src/grist-editor/ox-grist-renderer-crontab.ts +0 -18
  25. package/src/grist-editor/ox-grist-renderer-resource-code.ts +0 -85
  26. package/src/grist-editor/ox-grist-renderer-resource-id.ts +0 -17
  27. package/src/grist-editor/ox-grist-renderer-resource-object.ts +0 -26
  28. package/src/grist-editor/ox-popup-code-input.ts +0 -69
  29. package/src/grist-editor/ox-popup-privilege-input.ts +0 -93
  30. package/src/index.ts +0 -4
  31. package/src/input/index.ts +0 -3
  32. package/src/input/ox-input-background-pattern.ts +0 -196
  33. package/src/input/ox-input-fill-style.ts +0 -377
  34. package/src/input/ox-input-graphql.ts +0 -153
  35. package/src/property-editor/index.ts +0 -83
  36. package/src/property-editor/ox-property-editor-graphql.ts +0 -22
  37. package/src/property-editor/ox-property-editor-resource-object.ts +0 -148
  38. package/src/selector/ox-selector-resource-id.ts +0 -201
  39. package/src/selector/ox-selector-resource-object-legacy.ts +0 -367
  40. package/src/selector/ox-selector-resource-object.ts +0 -300
  41. package/stories/graphql-client.stories.ts +0 -73
  42. package/stories/ox-input-graphql.stories.ts +0 -70
  43. package/stories/ox-selector-resource-object.stories.ts +0 -98
  44. package/tsconfig.json +0 -24
  45. package/web-dev-server.config.mjs +0 -27
  46. package/web-test-runner.config.mjs +0 -41
@@ -1,148 +0,0 @@
1
- import '../selector/ox-selector-resource-object.js'
2
-
3
- import { html, TemplateResult } from 'lit'
4
- import { customElement } from 'lit/decorators.js'
5
-
6
- import { i18next } from '@operato/i18n'
7
- import { openPopup, PopupHandle } from '@operato/layout'
8
- import { OxPropertyEditor, PropertySpec } from '@operato/property-editor'
9
-
10
- @customElement('ox-property-editor-resource-object')
11
- export class OxPropertyEditorResourceObject extends OxPropertyEditor {
12
- static get styles() {
13
- return [...OxPropertyEditor.styles]
14
- }
15
-
16
- private popup?: PopupHandle
17
-
18
- editorTemplate(value: any, spec: PropertySpec): TemplateResult {
19
- var { nameField = 'name', descriptionField = 'description' } = spec.property || {}
20
- var name, description
21
-
22
- if (typeof nameField === 'function') {
23
- name = nameField(value)
24
- } else {
25
- name = value && value[nameField]
26
- }
27
-
28
- if (typeof descriptionField === 'function') {
29
- description = descriptionField(value)
30
- } else {
31
- description = value && value[descriptionField] && `(${value[descriptionField]})`
32
- }
33
-
34
- return html`
35
- ${!value
36
- ? html`<span tabindex="0" @click=${this.onClick.bind(this)} @keydown=${this.onKeyDown.bind(this)}></span>`
37
- : html`
38
- <span tabindex="0" @click=${this.onClick.bind(this)} @keydown=${this.onKeyDown.bind(this)}
39
- >${name || ''}${description || ''}</span
40
- >
41
- `}
42
- `
43
- }
44
-
45
- onClick(e: Event): void {
46
- e.stopPropagation()
47
- this.openSelector()
48
- }
49
-
50
- onKeyDown(e: KeyboardEvent): void {
51
- const key = e.key
52
- if (key == 'Enter') {
53
- e.stopPropagation()
54
- this.openSelector()
55
- }
56
- }
57
-
58
- openSelector() {
59
- if (this.popup) {
60
- delete this.popup
61
- }
62
-
63
- var {
64
- title,
65
- idField = 'id',
66
- nameField = 'name',
67
- descriptionField = 'description',
68
- valueField = 'id',
69
- queryName,
70
- basicArgs,
71
- select,
72
- columns,
73
- pagination,
74
- list
75
- } = this.property || {}
76
-
77
- /* select options is only for compatability, it might be able to be deprecated. */
78
- columns = columns || select
79
-
80
- const confirmCallback = (selected?: { [field: string]: any }) => {
81
- this.value = selected
82
- ? {
83
- ...(columns || [])
84
- .map((field: any) => field.name)
85
- .reduce(
86
- (obj: { [field: string]: any }, fieldName: string) => {
87
- return (obj = {
88
- ...obj,
89
- [fieldName]: selected[fieldName]
90
- })
91
- },
92
- {} as { [field: string]: any }
93
- ),
94
- [idField]: selected[idField],
95
- [nameField]: selected[nameField],
96
- [descriptionField]: selected[descriptionField]
97
- }
98
- : null
99
-
100
- this.dispatchEvent(
101
- new CustomEvent('change', {
102
- bubbles: true,
103
- composed: true
104
- })
105
- )
106
- }
107
-
108
- var value = this.value || {}
109
- var actualValue
110
- if (typeof valueField === 'function') {
111
- actualValue = valueField(value)
112
- } else {
113
- actualValue = value[valueField]
114
- }
115
-
116
- var template = html`
117
- <ox-selector-resource-object
118
- .value=${actualValue}
119
- .confirmCallback=${confirmCallback.bind(this)}
120
- .queryName=${queryName}
121
- .columns=${columns}
122
- .pagination=${pagination}
123
- .list=${list}
124
- .basicArgs=${basicArgs}
125
- .valueField=${valueField}
126
- ></ox-selector-resource-object>
127
- `
128
-
129
- this.popup = openPopup(template, {
130
- backdrop: true,
131
- size: 'large',
132
- search: {
133
- autofocus: true,
134
- placeholder: title || i18next.t('title.select_item'),
135
- handler: (instance: any, value: any) => {
136
- /* instance: template instance */
137
- instance.searchText(value)
138
- }
139
- },
140
- filter: {
141
- handler: (instance: any) => {
142
- /* instance: template instance */
143
- instance.toggleFilter()
144
- }
145
- }
146
- })
147
- }
148
- }
@@ -1,201 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
- import '@operato/data-grist'
3
-
4
- import gql from 'graphql-tag'
5
- import { css, html, LitElement } from 'lit'
6
- import { customElement, property, query } from 'lit/decorators.js'
7
-
8
- import { DataGrist, FetchOption, GristData, GristRecord, InheritedValueType, ZERO_DATA } from '@operato/data-grist'
9
- import { client } from '@operato/graphql'
10
- import { i18next } from '@operato/i18n'
11
- import { closePopup } from '@operato/popup'
12
- import { isMobileDevice } from '@operato/utils'
13
- import { CommonHeaderStyles } from '@operato/styles'
14
-
15
- @customElement('ox-selector-resource-id')
16
- export class OxSelectorResourceId extends LitElement {
17
- static styles = [
18
- CommonHeaderStyles,
19
- css`
20
- :host {
21
- display: flex;
22
- flex-direction: column;
23
-
24
- background-color: var(--md-sys-color-surface);
25
-
26
- width: var(--overlay-center-normal-width, 50%);
27
- height: var(--overlay-center-normal-height, 50%);
28
- }
29
-
30
- ox-grist {
31
- flex: 1;
32
- }
33
-
34
- #filters {
35
- display: flex;
36
- flex-direction: row;
37
- justify-content: space-between;
38
- }
39
-
40
- #filters > * {
41
- padding: var(--padding-default) var(--spacing-large);
42
- }
43
- `
44
- ]
45
-
46
- @property({ type: String }) value?: string
47
- @property({ type: Object }) config: any
48
- @property({ type: Object }) data: GristData = ZERO_DATA
49
- @property({ type: String }) queryName!: string
50
- @property({ type: Object }) basicArgs: any
51
- @property({ type: String }) inherited?: InheritedValueType = InheritedValueType.Include
52
- @property({ type: Object }) confirmCallback?: (record?: GristRecord) => void
53
- @property({ type: Array }) selectedRecords: GristRecord[] = []
54
-
55
- @query('ox-grist') grist!: DataGrist
56
-
57
- render() {
58
- return html`
59
- <ox-grist
60
- .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
61
- .config=${this.config}
62
- .fetchHandler=${this.fetchHandler.bind(this)}
63
- .selectedRecords=${this.selectedRecords}
64
- >
65
- <div id="filters" slot="headroom">
66
- <ox-filters-form autofocus></ox-filters-form>
67
- </div>
68
- </ox-grist>
69
-
70
- <div class="footer">
71
- <button @click=${this.onEmpty.bind(this)}>
72
- <md-icon>check_box_outline_blank</md-icon>${i18next.t('button.empty')}
73
- </button>
74
- <div filler></div>
75
- <button @click=${this.onCancel.bind(this)}><md-icon>cancel</md-icon>${i18next.t('button.cancel')}</button>
76
- <button @click=${this.onConfirm.bind(this)} done><md-icon>done</md-icon>${i18next.t('button.confirm')}</button>
77
- </div>
78
- `
79
- }
80
-
81
- onEmpty() {
82
- this.confirmCallback && this.confirmCallback()
83
- closePopup(this)
84
- }
85
-
86
- onCancel() {
87
- closePopup(this)
88
- }
89
-
90
- onConfirm() {
91
- this.confirmCallback && this.confirmCallback(this.selected)
92
- closePopup(this)
93
- }
94
-
95
- async firstUpdated() {
96
- this.config = {
97
- columns: [
98
- {
99
- type: 'gutter',
100
- gutterName: 'sequence'
101
- },
102
- {
103
- type: 'gutter',
104
- gutterName: 'row-selector',
105
- multiple: false
106
- },
107
- {
108
- type: 'string',
109
- name: 'id',
110
- header: i18next.t('field.id'),
111
- hidden: true
112
- },
113
- {
114
- type: 'string',
115
- name: 'name',
116
- header: i18next.t('field.name'),
117
- record: {
118
- align: 'left'
119
- },
120
- filter: 'search',
121
- sortable: true,
122
- width: 160
123
- },
124
- {
125
- type: 'string',
126
- name: 'description',
127
- header: i18next.t('field.description'),
128
- record: {
129
- align: 'left'
130
- },
131
- filter: 'search',
132
- sortable: true,
133
- width: 300
134
- }
135
- ],
136
- rows: {
137
- selectable: {
138
- multiple: false
139
- },
140
- handlers: {
141
- click: 'select-row'
142
- }
143
- },
144
- pagination: {
145
- infinite: true
146
- }
147
- }
148
-
149
- await this.updateComplete
150
-
151
- /* TODO config가 설정될 때, fetch() 가 동작하므로, fetch 완료 이벤트를 받아서, selected를 설정해주는 것이 좋겠다.
152
- 현재는 fetch() 가 두번 일어난다.
153
- */
154
- await this.grist.fetch()
155
-
156
- var selected = this.grist.data.records.find(item => this.value == item.id)
157
- if (selected) {
158
- this.selectedRecords = [selected]
159
- }
160
- /* TODO config가 설정될 때, fetch() 가 동작하므로, fetch 완료 이벤트를 받아서, selected를 설정해주는 것이 좋겠다. */
161
-
162
- await this.updateComplete
163
- this.grist.focus()
164
- }
165
-
166
- async fetchHandler({ page = 1, limit = 100, sorters = [], filters = [], inherited }: FetchOption) {
167
- const response = await client.query({
168
- query: gql`
169
- query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!], $inherited) {
170
- q: ${this.queryName}(filters: $filters, pagination: $pagination, sortings: $sortings, inhereted: $inherited) {
171
- items {
172
- id
173
- name
174
- description
175
- }
176
- total
177
- }
178
- }
179
- `,
180
- variables: {
181
- filters,
182
- pagination: { page, limit },
183
- sortings: sorters,
184
- inherited: inherited || this.inherited
185
- }
186
- })
187
-
188
- return {
189
- total: response.data.q.total || 0,
190
- records: response.data.q.items || []
191
- }
192
- }
193
-
194
- get selected() {
195
- var grist = this.renderRoot.querySelector('ox-grist') as DataGrist
196
-
197
- var selected = grist.selected
198
-
199
- return selected && selected.length > 0 ? selected[0] : undefined
200
- }
201
- }
@@ -1,367 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
- import '@operato/data-grist'
3
- import '@operato/form/ox-search-form.js'
4
-
5
- import gql from 'graphql-tag'
6
- import { css, html, LitElement } from 'lit'
7
- import { customElement, property, query } from 'lit/decorators.js'
8
-
9
- import {
10
- ColumnConfig,
11
- DataGrist,
12
- FetchHandler,
13
- FetchOption,
14
- GristData,
15
- GristEventHandler,
16
- GristRecord,
17
- InheritedValueType,
18
- ZERO_DATA
19
- } from '@operato/data-grist'
20
- import { MultiColumnFormStyles, OxSearchForm } from '@operato/form'
21
- import { client } from '@operato/graphql'
22
- import { i18next } from '@operato/i18n'
23
- import { closePopup } from '@operato/popup'
24
- import { isMobileDevice } from '@operato/utils'
25
- import { CommonHeaderStyles } from '@operato/styles'
26
-
27
- @customElement('ox-selector-resource-object-legacy')
28
- export class OxSelectorResourceObjectLegacy extends LitElement {
29
- static styles = [
30
- CommonHeaderStyles,
31
- MultiColumnFormStyles,
32
- css`
33
- :host {
34
- display: flex;
35
- flex-direction: column;
36
-
37
- background-color: var(--md-sys-color-surface);
38
- }
39
-
40
- ox-grist {
41
- flex: 1;
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: String }) inherited?: InheritedValueType = InheritedValueType.Include
61
- @property({ type: Object }) confirmCallback?: (record?: GristRecord | null) => void
62
- @property({ type: Array }) selectedRecords: GristRecord[] = []
63
-
64
- @property({ type: Array }) searchFields: any
65
- @property({ type: Array }) select: {
66
- type: string
67
- name: string
68
- header: string
69
- subFields?: string[]
70
- hidden: boolean
71
- ignoreCondition: boolean
72
- queryName: string
73
- width: number
74
- }[] = []
75
- @property({ type: Object }) list: any
76
- @property({ type: String }) valueField: string | ((item: any) => any) = 'id'
77
-
78
- @query('ox-grist') grist!: DataGrist
79
- @query('ox-search-form') searchForm!: OxSearchForm
80
-
81
- render() {
82
- return html`
83
- <ox-search-form
84
- id="search-form"
85
- @keypress=${(e: KeyboardEvent) => {
86
- if (e.key === 'Enter') {
87
- this.grist.fetch()
88
- }
89
- }}
90
- @submit=${(e: SubmitEvent) => this.grist.fetch()}
91
- .fields=${this.searchFields}
92
- autofocus
93
- ></ox-search-form>
94
-
95
- <ox-grist
96
- .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
97
- .config=${this.config}
98
- .data=${this.data}
99
- .fetchHandler=${this.fetchHandler.bind(this)}
100
- .selectedRecords=${this.selectedRecords}
101
- >
102
- <div slot="headroom" id="headroom">
103
- <div id="filters">
104
- <ox-filters-form></ox-filters-form>
105
- </div>
106
- </div>
107
- </ox-grist>
108
-
109
- <div class="footer">
110
- <button @click=${this.onEmpty.bind(this)}>
111
- <md-icon>check_box_outline_blank</md-icon>${i18next.t('button.empty')}
112
- </button>
113
- <div filler></div>
114
- <button @click=${this.onCancel.bind(this)}><md-icon>cancel</md-icon>${i18next.t('button.cancel')}</button>
115
- <button @click=${this.onConfirm.bind(this)}><md-icon>done</md-icon>${i18next.t('button.confirm')}</button>
116
- </div>
117
- `
118
- }
119
-
120
- onEmpty() {
121
- this.confirmCallback && this.confirmCallback()
122
- closePopup(this)
123
- }
124
-
125
- onCancel() {
126
- closePopup(this)
127
- }
128
-
129
- onConfirm() {
130
- this.confirmCallback && this.confirmCallback(this.selected)
131
- closePopup(this)
132
- }
133
-
134
- fetchHandler: FetchHandler = async ({ filters, page, limit, sorters = [], inherited }) => {
135
- const response = await client.query({
136
- query: gql`
137
- query($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!], $inherited: InheritedValueType) {
138
- fetch: ${
139
- this.queryName
140
- } (filters: $filters, pagination: $pagination, sortings: $sortings, inherited: $inherited)
141
- {
142
- ${this.getSelectFields()}
143
- }
144
- }
145
- `,
146
- variables: await this._buildConditions({ filters, page, limit, sorters, inherited: inherited || this.inherited })
147
- })
148
-
149
- if (!response.errors) {
150
- const records = response.data.fetch.items.map((item: any) => {
151
- let rowValue
152
-
153
- if (this.valueField && typeof this.valueField === 'function') {
154
- rowValue = this.valueField(item)
155
- } else if (this.valueField) {
156
- rowValue = item[this.valueField]
157
- } else {
158
- rowValue = item.id
159
- }
160
-
161
- if (this.value && this.value === rowValue) {
162
- this.selectedRecords = [item]
163
- item['__selected__'] = true
164
- }
165
-
166
- return item
167
- })
168
- const total = response.data.fetch.total
169
-
170
- return {
171
- records,
172
- total,
173
- limit,
174
- page
175
- }
176
- }
177
- }
178
-
179
- async firstUpdated() {
180
- this.config = {
181
- columns: [
182
- {
183
- type: 'gutter',
184
- gutterName: 'sequence'
185
- },
186
- {
187
- type: 'gutter',
188
- gutterName: 'row-selector',
189
- multiple: false
190
- }
191
- ],
192
- rows: {
193
- selectable: {
194
- multiple: false
195
- },
196
- handlers: {
197
- click: 'select-row',
198
- dblclick: ((columns, data, column, record, rowIndex, field) => {
199
- this.onConfirm()
200
- }) as GristEventHandler
201
- },
202
- appendable: false
203
- }
204
- }
205
-
206
- if (this.select && this.select.length > 0) {
207
- let _searchFields = this.select.filter(selectField => !selectField.hidden && !selectField.ignoreCondition)
208
- if (this.list && this.list.fields && this.list.fields.length > 0) {
209
- _searchFields = _searchFields.filter(searchField => this.list.fields.indexOf(searchField.name) >= 0)
210
- } else {
211
- _searchFields = _searchFields.slice(0, 4)
212
- }
213
-
214
- this.searchFields = _searchFields.map(selectField => {
215
- const fieldType = (selectField.type && selectField.type.toLowerCase()) || 'string'
216
- const numberTypes = ['integer', 'float']
217
- return {
218
- label: selectField.header || i18next.t(`field.${selectField.name}`),
219
- name: selectField.name,
220
- type:
221
- fieldType === 'string'
222
- ? 'text'
223
- : numberTypes.indexOf(fieldType) >= 0
224
- ? 'number'
225
- : fieldType === 'boolean'
226
- ? 'checkbox'
227
- : fieldType,
228
- queryName: selectField.queryName,
229
- props:
230
- fieldType === 'string'
231
- ? { searchOper: 'i_like' }
232
- : fieldType === 'object'
233
- ? { searchOper: 'in' }
234
- : { searchOper: 'eq' },
235
- attrs: fieldType === 'boolean' ? ['indeterminated'] : []
236
- }
237
- })
238
- this.config = {
239
- ...this.config,
240
- columns: [
241
- ...this.config.columns,
242
- ...this.select.map(selectField => {
243
- return {
244
- ...selectField,
245
- type: selectField.type || 'string',
246
- width: selectField.width || 160,
247
- header: selectField.header || i18next.t(`field.${selectField.name}`)
248
- }
249
- })
250
- ]
251
- }
252
- } else {
253
- this.searchFields = [
254
- {
255
- label: i18next.t('field.name'),
256
- name: 'name',
257
- type: 'text',
258
- props: { searchOper: 'i_like' }
259
- },
260
- {
261
- label: i18next.t('field.description'),
262
- name: 'description',
263
- type: 'text',
264
- props: { searchOper: 'i_like' }
265
- }
266
- ]
267
-
268
- this.config = {
269
- ...this.config,
270
- columns: [
271
- ...this.config.columns,
272
- {
273
- type: 'string',
274
- name: 'id',
275
- header: i18next.t('field.id'),
276
- hidden: true
277
- },
278
- {
279
- type: 'string',
280
- name: 'name',
281
- header: i18next.t('field.name'),
282
- record: {
283
- align: 'left'
284
- },
285
- sortable: true,
286
- width: 160
287
- },
288
- {
289
- type: 'string',
290
- name: 'description',
291
- header: i18next.t('field.description'),
292
- record: {
293
- align: 'left'
294
- },
295
- sortable: true,
296
- width: 300
297
- }
298
- ]
299
- }
300
- }
301
-
302
- this.config = {
303
- ...this.config,
304
- list: {
305
- ...this.list,
306
- fields:
307
- this.list && this.list.fields && this.list.fields.length > 0
308
- ? this.list.fields
309
- : (this.config.columns as ColumnConfig[])
310
- .filter(column => column.type !== 'gutter')
311
- .slice(0, 3)
312
- .map(column => column.name)
313
- }
314
- }
315
-
316
- await this.updateComplete
317
- this.grist && this.grist.focus()
318
- }
319
-
320
- getSelectFields() {
321
- if (this.select && this.select.length > 0) {
322
- return `items {
323
- ${this.select.map(selectField => {
324
- return selectField.type === 'object'
325
- ? `${selectField.name} { ${
326
- selectField.subFields && selectField.subFields.length > 0
327
- ? selectField.subFields.join(' ')
328
- : `id name description`
329
- } }`
330
- : `${selectField.name}`
331
- })}
332
- }
333
- total`
334
- } else {
335
- return `
336
- items {
337
- id
338
- name
339
- description
340
- }
341
- total
342
- `
343
- }
344
- }
345
-
346
- async _buildConditions({ filters, page, limit, sorters, inherited }: FetchOption) {
347
- const queryConditions = {
348
- filters: [],
349
- ...this.basicArgs
350
- }
351
-
352
- queryConditions.filters = [...queryConditions.filters, ...(await this.searchForm.getQueryFilters())]
353
- queryConditions.pagination = { page, limit }
354
- queryConditions.sortings = sorters
355
- queryConditions.inherited = inherited
356
-
357
- return queryConditions
358
- }
359
-
360
- get selected() {
361
- var grist = this.renderRoot.querySelector('ox-grist') as DataGrist
362
-
363
- var selected = grist.selected
364
-
365
- return selected && selected.length > 0 ? selected[0] : undefined
366
- }
367
- }