@operato/grist-editor 1.0.0-alpha.9

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 (69) hide show
  1. package/.editorconfig +29 -0
  2. package/.storybook/main.js +3 -0
  3. package/.storybook/server.mjs +8 -0
  4. package/CHANGELOG.md +11 -0
  5. package/LICENSE +21 -0
  6. package/README.md +75 -0
  7. package/demo/data-grist-test.html +456 -0
  8. package/demo/index.html +33 -0
  9. package/dist/src/code/code-editor.d.ts +6 -0
  10. package/dist/src/code/code-editor.js +50 -0
  11. package/dist/src/code/code-editor.js.map +1 -0
  12. package/dist/src/code/index.d.ts +1 -0
  13. package/dist/src/code/index.js +5 -0
  14. package/dist/src/code/index.js.map +1 -0
  15. package/dist/src/id/id-input.d.ts +19 -0
  16. package/dist/src/id/id-input.js +119 -0
  17. package/dist/src/id/id-input.js.map +1 -0
  18. package/dist/src/id/id-renderer.d.ts +2 -0
  19. package/dist/src/id/id-renderer.js +9 -0
  20. package/dist/src/id/id-renderer.js.map +1 -0
  21. package/dist/src/id/id-selector.d.ts +23 -0
  22. package/dist/src/id/id-selector.js +197 -0
  23. package/dist/src/id/id-selector.js.map +1 -0
  24. package/dist/src/id/index.d.ts +1 -0
  25. package/dist/src/id/index.js +7 -0
  26. package/dist/src/id/index.js.map +1 -0
  27. package/dist/src/index.d.ts +0 -0
  28. package/dist/src/index.js +2 -0
  29. package/dist/src/index.js.map +1 -0
  30. package/dist/src/object/index.d.ts +1 -0
  31. package/dist/src/object/index.js +6 -0
  32. package/dist/src/object/index.js.map +1 -0
  33. package/dist/src/object/object-editor.d.ts +18 -0
  34. package/dist/src/object/object-editor.js +152 -0
  35. package/dist/src/object/object-editor.js.map +1 -0
  36. package/dist/src/object/object-selector.d.ts +38 -0
  37. package/dist/src/object/object-selector.js +338 -0
  38. package/dist/src/object/object-selector.js.map +1 -0
  39. package/dist/src/parameters/index.d.ts +1 -0
  40. package/dist/src/parameters/index.js +6 -0
  41. package/dist/src/parameters/index.js.map +1 -0
  42. package/dist/src/parameters/parameters-editor-builder.d.ts +4 -0
  43. package/dist/src/parameters/parameters-editor-builder.js +119 -0
  44. package/dist/src/parameters/parameters-editor-builder.js.map +1 -0
  45. package/dist/src/parameters/parameters-editor-popup.d.ts +13 -0
  46. package/dist/src/parameters/parameters-editor-popup.js +111 -0
  47. package/dist/src/parameters/parameters-editor-popup.js.map +1 -0
  48. package/dist/src/parameters/parameters-editor.d.ts +18 -0
  49. package/dist/src/parameters/parameters-editor.js +106 -0
  50. package/dist/src/parameters/parameters-editor.js.map +1 -0
  51. package/dist/tsconfig.tsbuildinfo +1 -0
  52. package/package.json +87 -0
  53. package/src/code/code-editor.ts +78 -0
  54. package/src/code/index.ts +6 -0
  55. package/src/id/id-input.ts +135 -0
  56. package/src/id/id-renderer.ts +12 -0
  57. package/src/id/id-selector.ts +189 -0
  58. package/src/id/index.ts +8 -0
  59. package/src/index.ts +0 -0
  60. package/src/object/index.ts +8 -0
  61. package/src/object/object-editor.ts +162 -0
  62. package/src/object/object-selector.ts +346 -0
  63. package/src/parameters/index.ts +8 -0
  64. package/src/parameters/parameters-editor-builder.ts +130 -0
  65. package/src/parameters/parameters-editor-popup.ts +106 -0
  66. package/src/parameters/parameters-editor.ts +112 -0
  67. package/tsconfig.json +23 -0
  68. package/web-dev-server.config.mjs +27 -0
  69. package/web-test-runner.config.mjs +41 -0
@@ -0,0 +1,78 @@
1
+ import '@material/mwc-icon'
2
+ import {} from '@operato/shell'
3
+
4
+ import { InputEditor, RecordConfig } from '@operato/data-grist'
5
+ import { client, gqlContext } from '@operato/graphql'
6
+
7
+ import gql from 'graphql-tag'
8
+ import { html } from 'lit'
9
+
10
+ type CommonCodeDetail = {
11
+ name: string
12
+ description: string
13
+ rank: number
14
+ }
15
+
16
+ type CommonCode = {
17
+ name: string
18
+ description: string
19
+ details: CommonCodeDetail[]
20
+ }
21
+
22
+ const FETCH_COMMON_CODE_GQL = (codeName: string) => gql`
23
+ {
24
+ commonCode(name: "${codeName}") {
25
+ details {
26
+ name
27
+ description
28
+ rank
29
+ }
30
+ }
31
+ }
32
+ `
33
+
34
+ export class CodeEditor extends InputEditor {
35
+ async firstUpdated() {
36
+ super.firstUpdated()
37
+
38
+ var { codeName, codes } = this.column.record || {}
39
+
40
+ if (!codes && codeName) {
41
+ /* codeName으로 fetch 해와서, this.column.record.codes에 보관한다. */
42
+ var response = await client.query({
43
+ query: FETCH_COMMON_CODE_GQL(codeName),
44
+ context: gqlContext()
45
+ })
46
+
47
+ var commonCode = response && response.data && response.data.commonCode
48
+
49
+ if (commonCode) {
50
+ this.column.record.codes = [{ name: '', description: '' }].concat(
51
+ commonCode.details.sort(function (a: CommonCodeDetail, b: CommonCodeDetail) {
52
+ return a.rank - b.rank
53
+ })
54
+ )
55
+
56
+ this.requestUpdate()
57
+ }
58
+ }
59
+ }
60
+
61
+ get editorTemplate() {
62
+ var { codes } = this.column.record || {}
63
+
64
+ return html`
65
+ <select>
66
+ ${(codes || ['']).map(
67
+ (code: CommonCode) => html`
68
+ <option ?selected=${code.name == this.value} value=${code.name}>
69
+ ${code.name}${code.description ? ` - ${code.description}` : ''}
70
+ </option>
71
+ `
72
+ )}
73
+ </select>
74
+ `
75
+ }
76
+ }
77
+
78
+ customElements.define('code-editor', CodeEditor)
@@ -0,0 +1,6 @@
1
+ import { registerEditor, registerRenderer } from '@operato/data-grist'
2
+
3
+ import { CodeEditor } from './code-editor'
4
+
5
+ /* register grist renderer/editor for id */
6
+ registerEditor('code', CodeEditor)
@@ -0,0 +1,135 @@
1
+ import '@material/mwc-icon'
2
+ import './id-selector.js'
3
+
4
+ import { ColumnConfig, GristRecord, InputEditor } from '@operato/data-grist'
5
+ import { TemplateResult, css, html } from 'lit'
6
+ import { customElement, property } from 'lit/decorators.js'
7
+
8
+ import { openPopup } from '@operato/layout'
9
+
10
+ @customElement('ox-id-input')
11
+ export class IdInput extends InputEditor {
12
+ static styles = css`
13
+ :host {
14
+ display: flex;
15
+ flex-flow: row nowrap;
16
+ align-items: center;
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
+
30
+ justify-content: inherit;
31
+ }
32
+
33
+ span {
34
+ display: flex;
35
+ flex: auto;
36
+
37
+ justify-content: inherit;
38
+ }
39
+
40
+ mwc-icon {
41
+ width: 20px;
42
+ font-size: 1.5em;
43
+ margin-left: auto;
44
+ }
45
+ `
46
+
47
+ @property({ type: Object }) value?: { id?: string; [propany: string]: any }
48
+ @property({ type: Object }) column!: ColumnConfig
49
+ @property({ type: Object }) record!: GristRecord
50
+ @property({ type: Number }) row!: number
51
+
52
+ private popup?: {
53
+ name: string
54
+ close: () => void
55
+ closed: boolean
56
+ onclosed?: (() => void) | undefined
57
+ }
58
+
59
+ private template?: TemplateResult
60
+
61
+ render() {
62
+ var { nameField = 'name', descriptionField = 'description' } = this.column.record.options || {}
63
+ var value = this.value
64
+
65
+ return html`
66
+ ${!value ? html`` : html` <span>${value[nameField]} (${value[descriptionField]})</span> `}
67
+ <mwc-icon>arrow_drop_down</mwc-icon>
68
+ `
69
+ }
70
+
71
+ async firstUpdated() {
72
+ this.value = this.record[this.column.name]
73
+ this.template = ((this.column.record || {}).options || {}).template
74
+
75
+ await this.updateComplete
76
+
77
+ this.renderRoot.addEventListener('click', e => {
78
+ e.stopPropagation()
79
+
80
+ this.openSelector()
81
+ })
82
+
83
+ this.openSelector()
84
+ }
85
+
86
+ openSelector() {
87
+ if (this.popup) {
88
+ delete this.popup
89
+ }
90
+
91
+ const confirmCallback: (record: GristRecord | undefined) => void = selected => {
92
+ if (!selected) {
93
+ return
94
+ }
95
+
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
+ [idField]: selected[idField],
106
+ [nameField]: selected[nameField],
107
+ [descriptionField]: selected[descriptionField]
108
+ },
109
+ record: this.record,
110
+ column: this.column,
111
+ row: this.row
112
+ }
113
+ })
114
+ )
115
+ }
116
+
117
+ var value = this.value || {}
118
+ var template =
119
+ this.template ||
120
+ html`
121
+ <ox-id-selector
122
+ .value=${value.id}
123
+ .confirmCallback=${confirmCallback.bind(this)}
124
+ .queryName=${this.column.record.options.queryName}
125
+ .basicArgs=${this.column.record.options.basicArgs}
126
+ ></ox-id-selector>
127
+ `
128
+
129
+ this.popup = openPopup(template, {
130
+ backdrop: true,
131
+ size: 'large',
132
+ title: 'select item'
133
+ })
134
+ }
135
+ }
@@ -0,0 +1,12 @@
1
+ import { FieldRenderer } from '@operato/data-grist'
2
+ import { html } from 'lit'
3
+
4
+ export const IdRenderer: FieldRenderer = (value, column, record, rowIndex, field) => {
5
+ if (!value) {
6
+ return ''
7
+ }
8
+
9
+ var { nameField = 'name', descriptionField = 'description' } = column.record.options || {}
10
+
11
+ return html` ${value[nameField] || ''}(${value[descriptionField] || ''}) `
12
+ }
@@ -0,0 +1,189 @@
1
+ import '@operato/data-grist'
2
+
3
+ import { DataGrist, FetchOption, GristData, GristRecord, ZERO_DATA } from '@operato/data-grist'
4
+ import { LitElement, css, html } from 'lit'
5
+ import { customElement, property, query } from 'lit/decorators.js'
6
+
7
+ import { client } from '@operato/graphql'
8
+ import gql from 'graphql-tag'
9
+ import { i18next } from '@operato/i18n'
10
+ import { isMobileDevice } from '@operato/utils'
11
+
12
+ @customElement('ox-id-selector')
13
+ export class IdSelector extends LitElement {
14
+ static styles = css`
15
+ :host {
16
+ display: flex;
17
+ flex-direction: column;
18
+
19
+ background-color: #fff;
20
+
21
+ width: var(--overlay-center-normal-width, 50%);
22
+ height: var(--overlay-center-normal-height, 50%);
23
+ }
24
+
25
+ ox-grist {
26
+ flex: 1;
27
+ }
28
+
29
+ #filters {
30
+ display: flex;
31
+ flex-direction: row;
32
+ justify-content: space-between;
33
+ }
34
+
35
+ #filters > * {
36
+ padding: var(--padding-default) var(--padding-wide);
37
+ }
38
+
39
+ .button-container {
40
+ display: flex;
41
+ margin-left: auto;
42
+ }
43
+ `
44
+
45
+ @property({ type: String }) value?: string
46
+ @property({ type: Object }) config: any
47
+ @property({ type: Object }) data: GristData = ZERO_DATA
48
+ @property({ type: String }) queryName!: string
49
+ @property({ type: Object }) basicArgs: any
50
+ @property({ type: Object }) confirmCallback?: (record: GristRecord | undefined) => void
51
+ @property({ type: Array }) selectedRecords: GristRecord[] = []
52
+
53
+ @query('ox-grist') grist!: DataGrist
54
+
55
+ render() {
56
+ return html`
57
+ <ox-grist
58
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
59
+ .config=${this.config}
60
+ .fetchHandler=${this.fetchHandler.bind(this)}
61
+ .selectedRecords=${this.selectedRecords}
62
+ >
63
+ <div id="filters" slot="headroom">
64
+ <ox-filters-form></ox-filters-form>
65
+ </div>
66
+ </ox-grist>
67
+
68
+ <div class="button-container">
69
+ <mwc-button @click=${this.oncancel.bind(this)}>${i18next.t('button.cancel')}</mwc-button>
70
+ <mwc-button @click=${this.onconfirm.bind(this)}>${i18next.t('button.confirm')}</mwc-button>
71
+ </div>
72
+ `
73
+ }
74
+
75
+ oncancel() {
76
+ history.back()
77
+ }
78
+
79
+ onconfirm() {
80
+ this.confirmCallback && this.confirmCallback(this.selected)
81
+ history.back()
82
+ }
83
+
84
+ async firstUpdated() {
85
+ this.config = {
86
+ columns: [
87
+ {
88
+ type: 'gutter',
89
+ gutterName: 'sequence'
90
+ },
91
+ {
92
+ type: 'gutter',
93
+ gutterName: 'row-selector',
94
+ multiple: false
95
+ },
96
+ {
97
+ type: 'string',
98
+ name: 'id',
99
+ header: i18next.t('field.id'),
100
+ hidden: true
101
+ },
102
+ {
103
+ type: 'string',
104
+ name: 'name',
105
+ header: i18next.t('field.name'),
106
+ record: {
107
+ align: 'left'
108
+ },
109
+ filter: 'search',
110
+ sortable: true,
111
+ width: 160
112
+ },
113
+ {
114
+ type: 'string',
115
+ name: 'description',
116
+ header: i18next.t('field.description'),
117
+ record: {
118
+ align: 'left'
119
+ },
120
+ filter: 'search',
121
+ sortable: true,
122
+ width: 300
123
+ }
124
+ ],
125
+ rows: {
126
+ selectable: {
127
+ multiple: false
128
+ },
129
+ handlers: {
130
+ click: 'select-row'
131
+ }
132
+ },
133
+ pagination: {
134
+ infinite: true
135
+ }
136
+ }
137
+
138
+ await this.updateComplete
139
+
140
+ /* TODO config가 설정될 때, fetch() 가 동작하므로, fetch 완료 이벤트를 받아서, selected를 설정해주는 것이 좋겠다.
141
+ 현재는 fetch() 가 두번 일어난다.
142
+ */
143
+ await this.grist.fetch()
144
+
145
+ var selected = this.grist.data.records.find(item => this.value == item.id)
146
+ if (selected) {
147
+ this.selectedRecords = [selected]
148
+ }
149
+ /* TODO config가 설정될 때, fetch() 가 동작하므로, fetch 완료 이벤트를 받아서, selected를 설정해주는 것이 좋겠다. */
150
+
151
+ await this.updateComplete
152
+ this.grist.focus()
153
+ }
154
+
155
+ async fetchHandler({ page = 1, limit = 100, sorters = [], filters = [] }: FetchOption) {
156
+ const response = await client.query({
157
+ query: gql`
158
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
159
+ q: ${this.queryName}(filters: $filters, pagination: $pagination, sortings: $sortings) {
160
+ items {
161
+ id
162
+ name
163
+ description
164
+ }
165
+ total
166
+ }
167
+ }
168
+ `,
169
+ variables: {
170
+ filters,
171
+ pagination: { page, limit },
172
+ sortings: sorters
173
+ }
174
+ })
175
+
176
+ return {
177
+ total: response.data.q.total || 0,
178
+ records: response.data.q.items || []
179
+ }
180
+ }
181
+
182
+ get selected() {
183
+ var grist = this.renderRoot.querySelector('ox-grist') as DataGrist
184
+
185
+ var selected = grist.selected
186
+
187
+ return selected && selected.length > 0 ? selected[0] : undefined
188
+ }
189
+ }
@@ -0,0 +1,8 @@
1
+ import { registerEditor, registerRenderer } from '@operato/data-grist'
2
+
3
+ import { IdInput } from './id-input.js'
4
+ import { IdRenderer } from './id-renderer.js'
5
+
6
+ /* register grist renderer/editor for id */
7
+ registerEditor('id', IdInput)
8
+ registerRenderer('id', IdRenderer)
package/src/index.ts ADDED
File without changes
@@ -0,0 +1,8 @@
1
+ import { registerEditor, registerRenderer } from '@operato/data-grist'
2
+
3
+ import { ObjectEditor } from './object-editor'
4
+
5
+ /* register grist renderer/editor for id */
6
+ registerEditor('object', ObjectEditor)
7
+
8
+ // registerRenderer('id', IdRenderer)
@@ -0,0 +1,162 @@
1
+ import '@material/mwc-icon'
2
+ import '../../elements/object-selector'
3
+
4
+ import {
5
+ ColumnConfig,
6
+ GristData,
7
+ GristRecord,
8
+ InputEditor,
9
+ ZERO_COLUMN,
10
+ ZERO_CONFIG,
11
+ ZERO_RECORD
12
+ } from '@operato/data-grist'
13
+ import { LitElement, TemplateResult, css, html } from 'lit'
14
+ import { PopupHandle, openPopup } from '@operato/layout'
15
+ import { customElement, property } from 'lit/decorators.js'
16
+
17
+ import { i18next } from '@operato/i18n'
18
+
19
+ @customElement('ox-object-editor')
20
+ export class ObjectEditor extends InputEditor {
21
+ static styles = css`
22
+ :host {
23
+ display: flex;
24
+ flex-flow: row nowrap;
25
+
26
+ padding: 7px 0px;
27
+ box-sizing: border-box;
28
+
29
+ width: 100%;
30
+ height: 100%;
31
+
32
+ border: 0;
33
+ background-color: transparent;
34
+
35
+ font: var(--grist-object-editor-font);
36
+ color: var(--grist-object-editor-color);
37
+ justify-content: inherit;
38
+ }
39
+
40
+ span {
41
+ display: flex;
42
+ flex: auto;
43
+
44
+ justify-content: inherit;
45
+ }
46
+
47
+ mwc-icon {
48
+ width: 20px;
49
+ font-size: 1.5em;
50
+ margin-left: auto;
51
+ }
52
+ `
53
+
54
+ @property({ type: Object }) value: { [prop: string]: any } = {}
55
+ @property({ type: Object }) column: ColumnConfig = ZERO_COLUMN
56
+ @property({ type: Object }) record: GristRecord = ZERO_RECORD
57
+ @property({ type: Number }) row?: number
58
+
59
+ private popup?: PopupHandle
60
+ private template?: TemplateResult
61
+
62
+ render() {
63
+ var value = this.value || {}
64
+
65
+ var { nameField = 'name', descriptionField = 'description' } = this.column.record.options || {}
66
+ var name, description
67
+ if (typeof nameField === 'function') {
68
+ name = nameField(value)
69
+ } else {
70
+ name = value[nameField]
71
+ }
72
+
73
+ if (typeof descriptionField === 'function') {
74
+ description = descriptionField(value)
75
+ } else {
76
+ description = value[descriptionField] && `(${value[descriptionField]})`
77
+ }
78
+
79
+ return html`
80
+ ${!value ? html`` : html` <span>${name || ''}${description || ''}</span> `}
81
+ <mwc-icon>arrow_drop_down</mwc-icon>
82
+ `
83
+ }
84
+
85
+ async firstUpdated() {
86
+ this.value = this.record[this.column.name]
87
+ this.template = ((this.column.record || {}).options || {}).template
88
+
89
+ await this.updateComplete
90
+
91
+ this.renderRoot.addEventListener('click', e => {
92
+ e.stopPropagation()
93
+
94
+ this.openSelector()
95
+ })
96
+ }
97
+
98
+ openSelector() {
99
+ if (this.popup) {
100
+ delete this.popup
101
+ }
102
+
103
+ const confirmCallback = (selected: { [field: string]: any }) => {
104
+ var { idField = 'id', nameField = 'name', descriptionField = 'description' } = this.column.record.options || {}
105
+
106
+ this.dispatchEvent(
107
+ new CustomEvent('field-change', {
108
+ bubbles: true,
109
+ composed: true,
110
+ detail: {
111
+ before: this.value,
112
+ after: {
113
+ ...(this.column.record.options.select || [])
114
+ .map((field: any) => field.name)
115
+ .reduce((obj: { [field: string]: any }, fieldName: string) => {
116
+ return (obj = {
117
+ ...obj,
118
+ [fieldName]: selected[fieldName]
119
+ })
120
+ }, {} as { [field: string]: any }),
121
+ [idField]: selected[idField],
122
+ [nameField]: selected[nameField],
123
+ [descriptionField]: selected[descriptionField]
124
+ },
125
+ record: this.record,
126
+ column: this.column,
127
+ row: this.row
128
+ }
129
+ })
130
+ )
131
+ }
132
+
133
+ var value = this.value || {}
134
+ var valueField: string | ((item: any) => any) = this.column.record.options.valueField || 'id'
135
+ var actualValue
136
+ if (typeof valueField === 'function') {
137
+ actualValue = valueField(value)
138
+ } else {
139
+ actualValue = value[valueField]
140
+ }
141
+
142
+ var template =
143
+ this.template ||
144
+ html`
145
+ <object-selector
146
+ .value=${actualValue}
147
+ .confirmCallback=${confirmCallback.bind(this)}
148
+ .queryName=${this.column.record.options.queryName}
149
+ .select=${this.column.record.options.select}
150
+ .list=${this.column.record.options.list}
151
+ .basicArgs=${this.column.record.options.basicArgs}
152
+ .valueField=${this.column.record.options.valueField}
153
+ ></object-selector>
154
+ `
155
+
156
+ this.popup = openPopup(template, {
157
+ backdrop: true,
158
+ size: 'large',
159
+ title: i18next.t('title.select_item')
160
+ })
161
+ }
162
+ }