@operato/data-grist 7.1.26 → 7.1.28

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 (76) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/src/data-grist.d.ts +16 -1
  3. package/dist/src/data-grist.js +53 -20
  4. package/dist/src/data-grist.js.map +1 -1
  5. package/dist/src/record-view/index.d.ts +1 -1
  6. package/dist/src/record-view/index.js +1 -1
  7. package/dist/src/record-view/index.js.map +1 -1
  8. package/dist/src/record-view/ox-record-creator.d.ts +26 -0
  9. package/dist/src/record-view/{record-creator.js → ox-record-creator.js} +114 -15
  10. package/dist/src/record-view/ox-record-creator.js.map +1 -0
  11. package/dist/src/record-view/record-view-body.d.ts +6 -1
  12. package/dist/src/record-view/record-view-body.js +43 -4
  13. package/dist/src/record-view/record-view-body.js.map +1 -1
  14. package/dist/src/record-view/record-view.d.ts +7 -1
  15. package/dist/src/record-view/record-view.js +7 -1
  16. package/dist/src/record-view/record-view.js.map +1 -1
  17. package/dist/src/types.d.ts +6 -0
  18. package/dist/src/types.js +7 -0
  19. package/dist/src/types.js.map +1 -1
  20. package/dist/stories/accumulator-format.stories.d.ts +1 -1
  21. package/dist/stories/accumulator-format.stories.js +1 -1
  22. package/dist/stories/accumulator-format.stories.js.map +1 -1
  23. package/dist/stories/click-event-custom.stories.d.ts +45 -0
  24. package/dist/stories/click-event-custom.stories.js +247 -0
  25. package/dist/stories/click-event-custom.stories.js.map +1 -0
  26. package/dist/stories/click-event.stories.d.ts +1 -1
  27. package/dist/stories/click-event.stories.js +1 -1
  28. package/dist/stories/click-event.stories.js.map +1 -1
  29. package/dist/stories/fixed-column.stories.d.ts +1 -1
  30. package/dist/stories/fixed-column.stories.js +1 -1
  31. package/dist/stories/fixed-column.stories.js.map +1 -1
  32. package/dist/stories/grid-setting.stories.d.ts +1 -1
  33. package/dist/stories/grid-setting.stories.js +1 -1
  34. package/dist/stories/grid-setting.stories.js.map +1 -1
  35. package/dist/stories/grist-modes.stories.d.ts +1 -1
  36. package/dist/stories/grist-modes.stories.js +1 -1
  37. package/dist/stories/grist-modes.stories.js.map +1 -1
  38. package/dist/stories/group-header.stories.d.ts +1 -1
  39. package/dist/stories/group-header.stories.js +1 -1
  40. package/dist/stories/group-header.stories.js.map +1 -1
  41. package/dist/stories/textarea.stories.d.ts +1 -1
  42. package/dist/stories/textarea.stories.js +1 -1
  43. package/dist/stories/textarea.stories.js.map +1 -1
  44. package/dist/stories/tree-column-with-checkbox.stories.d.ts +1 -1
  45. package/dist/stories/tree-column-with-checkbox.stories.js +1 -1
  46. package/dist/stories/tree-column-with-checkbox.stories.js.map +1 -1
  47. package/dist/stories/tree-column.stories.d.ts +1 -1
  48. package/dist/stories/tree-column.stories.js +1 -1
  49. package/dist/stories/tree-column.stories.js.map +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +8 -8
  52. package/src/data-grist.ts +60 -24
  53. package/src/record-view/index.ts +1 -1
  54. package/src/record-view/{record-creator.ts → ox-record-creator.ts} +119 -10
  55. package/src/record-view/record-view-body.ts +50 -4
  56. package/src/record-view/record-view.ts +10 -2
  57. package/src/types.ts +8 -0
  58. package/stories/accumulator-format.stories.ts +1 -1
  59. package/stories/click-event-custom.stories.ts +287 -0
  60. package/stories/click-event.stories.ts +1 -1
  61. package/stories/fixed-column.stories.ts +1 -1
  62. package/stories/grid-setting.stories.ts +1 -1
  63. package/stories/grist-modes.stories.ts +1 -1
  64. package/stories/group-header.stories.ts +1 -1
  65. package/stories/textarea.stories.ts +1 -1
  66. package/stories/tree-column-with-checkbox.stories.ts +1 -1
  67. package/stories/tree-column.stories.ts +1 -1
  68. package/themes/calendar-theme.css +3 -1
  69. package/translations/en.json +5 -1
  70. package/translations/ja.json +5 -1
  71. package/translations/ko.json +5 -1
  72. package/translations/ms.json +5 -1
  73. package/translations/zh.json +5 -1
  74. package/dist/src/record-view/record-creator.d.ts +0 -17
  75. package/dist/src/record-view/record-creator.js.map +0 -1
  76. package/yarn-error.log +0 -16971
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/data-grist",
3
- "version": "7.1.26",
3
+ "version": "7.1.28",
4
4
  "description": "User interface for grid (desktop) and list (mobile)",
5
5
  "author": "heartyoh",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "./ox-report.js": "./dist/src/data-report.js",
24
24
  "./ox-filters-form.js": "./dist/src/filters/filters-form.js",
25
25
  "./ox-sorters-control.js": "./dist/src/sorters/sorters-control.js",
26
- "./ox-record-creator.js": "./dist/src/record-view/record-creator.js",
26
+ "./ox-record-creator.js": "./dist/src/record-view/ox-record-creator.js",
27
27
  "./ox-grist-personalizer.js": "./dist/src/personalizer/ox-grist-personalizer.js"
28
28
  },
29
29
  "typesVersions": {
@@ -41,7 +41,7 @@
41
41
  "dist/src/sorters/sorters-control.d.ts"
42
42
  ],
43
43
  "ox-record-creator.js": [
44
- "dist/src/record-view/record-creator.d.ts"
44
+ "dist/src/record-view/ox-record-creator.d.ts"
45
45
  ],
46
46
  "ox-grist-personalizer.js": [
47
47
  "dist/src/personalizer/ox-grist-personalizer.d.ts"
@@ -63,11 +63,11 @@
63
63
  "dependencies": {
64
64
  "@material/web": "^2.0.0",
65
65
  "@operato/headroom": "^7.1.1",
66
- "@operato/input": "^7.1.26",
67
- "@operato/p13n": "^7.1.26",
68
- "@operato/popup": "^7.1.26",
66
+ "@operato/input": "^7.1.27",
67
+ "@operato/p13n": "^7.1.27",
68
+ "@operato/popup": "^7.1.27",
69
69
  "@operato/pull-to-refresh": "^7.1.1",
70
- "@operato/styles": "^7.1.25",
70
+ "@operato/styles": "^7.1.27",
71
71
  "@operato/time-calculator": "^7.1.1",
72
72
  "@operato/utils": "^7.1.1",
73
73
  "i18next": "^23.11.5",
@@ -108,5 +108,5 @@
108
108
  "prettier --write"
109
109
  ]
110
110
  },
111
- "gitHead": "5490ff22eff31a7e2006d4da0af115f980d5945c"
111
+ "gitHead": "948ee8dd56704f8aed966d7b85b8025999b94e7f"
112
112
  }
package/src/data-grist.ts CHANGED
@@ -31,7 +31,8 @@ import {
31
31
  GristSelectFunction,
32
32
  PaginationConfig,
33
33
  PersonalGristPreference,
34
- SortersConfig
34
+ SortersConfig,
35
+ ValidationReason
35
36
  } from './types'
36
37
  import { convertListParamToSearchString, convertSearchStringToListParam } from './utils'
37
38
 
@@ -478,28 +479,28 @@ export class DataGrist extends LitElement implements DataConsumer {
478
479
  </ox-grid>
479
480
  `
480
481
  : this.mode == 'CARD'
481
- ? html`
482
- <ox-card
483
- id="grist"
484
- .config=${this.compiledConfig}
485
- .data=${this._data}
486
- .sorters=${this.sorters || []}
487
- .filters=${this.filters || []}
488
- ?empty=${empty}
489
- >
490
- </ox-card>
491
- `
492
- : html`
493
- <ox-list
494
- id="grist"
495
- .config=${this.compiledConfig}
496
- .data=${this._data}
497
- .sorters=${this.sorters || []}
498
- .filters=${this.filters || []}
499
- ?empty=${empty}
500
- >
501
- </ox-list>
502
- `}
482
+ ? html`
483
+ <ox-card
484
+ id="grist"
485
+ .config=${this.compiledConfig}
486
+ .data=${this._data}
487
+ .sorters=${this.sorters || []}
488
+ .filters=${this.filters || []}
489
+ ?empty=${empty}
490
+ >
491
+ </ox-card>
492
+ `
493
+ : html`
494
+ <ox-list
495
+ id="grist"
496
+ .config=${this.compiledConfig}
497
+ .data=${this._data}
498
+ .sorters=${this.sorters || []}
499
+ .filters=${this.filters || []}
500
+ ?empty=${empty}
501
+ >
502
+ </ox-list>
503
+ `}
503
504
  </div>
504
505
 
505
506
  <div id="spinner" ?show=${this._showSpinner}></div>
@@ -1190,8 +1191,43 @@ export class DataGrist extends LitElement implements DataConsumer {
1190
1191
  })
1191
1192
  )
1192
1193
  }
1193
-
1194
+ /**
1195
+ * Returns the current pagination limit.
1196
+ * @returns {number} The current pagination limit value
1197
+ */
1194
1198
  getCurrentLimit() {
1195
1199
  return this.dataProvider?.limit || ZERO_PAGINATION.limit
1196
1200
  }
1201
+
1202
+ /**
1203
+ * Checks the validity of dirty records.
1204
+ * @returns {Array<{record: GristRecord, invalidFields: Array<{field: string, reason: ValidationReason}>}>} List of invalid records and their corresponding invalid fields
1205
+ */
1206
+ checkDirtyRecordsValidity(): { record: GristRecord; invalidFields: { field: string; reason: ValidationReason }[] }[] {
1207
+ const records = this.dirtyRecords
1208
+ const validationResults = []
1209
+
1210
+ for (const record of records) {
1211
+ const invalidFields = []
1212
+
1213
+ for (const column of this.compiledConfig.columns) {
1214
+ if (column.record?.mandatory && (record[column.name] === undefined || record[column.name] === null)) {
1215
+ invalidFields.push({
1216
+ field: column.name,
1217
+ reason: ValidationReason.MANDATORY
1218
+ })
1219
+ }
1220
+ // Additional validation rules can be implemented here.
1221
+ }
1222
+
1223
+ if (invalidFields.length > 0) {
1224
+ validationResults.push({
1225
+ record,
1226
+ invalidFields
1227
+ })
1228
+ }
1229
+ }
1230
+
1231
+ return validationResults
1232
+ }
1197
1233
  }
@@ -1,2 +1,2 @@
1
1
  export * from './record-view'
2
- export * from './record-creator'
2
+ export * from './ox-record-creator'
@@ -1,20 +1,29 @@
1
1
  import '@material/web/icon/icon.js'
2
2
  import './record-view'
3
3
 
4
- import { html, LitElement } from 'lit'
4
+ import { css, html, LitElement } from 'lit'
5
5
  import { customElement, property, state } from 'lit/decorators.js'
6
6
 
7
7
  import { OxPopup } from '@operato/popup'
8
8
 
9
9
  import { DataGrist } from '../data-grist'
10
- import { ColumnConfig, GristRecord } from '../types'
10
+ import { ColumnConfig, GristRecord, ValidationReason } from '../types'
11
11
  import { RecordView } from './record-view'
12
12
 
13
13
  @customElement('ox-record-creator')
14
- export class RecordCreator extends LitElement {
14
+ export class OxRecordCreator extends LitElement {
15
+ static styles = [
16
+ css`
17
+ ::slotted([slot='popup']) {
18
+ display: none;
19
+ }
20
+ `
21
+ ]
22
+
15
23
  @state() grist?: DataGrist
16
24
 
17
- @property({ type: Object }) callback?: (operation: { [key: string]: any }) => boolean
25
+ @property({ type: Object }) callback?: (record: GristRecord) => boolean
26
+ @property({ type: Object }) customPopupCallback?: (popup: any) => boolean
18
27
  @property({ type: Boolean, attribute: 'light-popup' }) lightPopup: boolean = false
19
28
  @property({ type: Boolean, attribute: 'prevent-close-on-blur' }) preventCloseOnBlur = false
20
29
 
@@ -26,9 +35,9 @@ export class RecordCreator extends LitElement {
26
35
  e.stopPropagation()
27
36
 
28
37
  if (this.lightPopup) {
29
- this.lightPopupRecordView()
38
+ this.openLightPopup()
30
39
  } else {
31
- this.popupRecordView()
40
+ this.openPopup()
32
41
  }
33
42
  })
34
43
  }
@@ -40,7 +49,55 @@ export class RecordCreator extends LitElement {
40
49
  }
41
50
 
42
51
  render() {
43
- return html`<slot></slot>`
52
+ return html`
53
+ <slot></slot>
54
+ <slot name="popup"></slot>
55
+ `
56
+ }
57
+
58
+ validateRecord(record: GristRecord): { field: string; reason: ValidationReason }[] {
59
+ const columns = this.grist!.compiledConfig.columns
60
+ const invalidFields: { field: string; reason: ValidationReason }[] = []
61
+
62
+ columns
63
+ .filter(column => !column.hidden)
64
+ .forEach(column => {
65
+ if (
66
+ column.record?.mandatory &&
67
+ (record[column.name] === undefined || record[column.name] === null || record[column.name] === '')
68
+ ) {
69
+ invalidFields.push({
70
+ field: column.name,
71
+ reason: ValidationReason.MANDATORY
72
+ })
73
+ }
74
+ })
75
+
76
+ return invalidFields
77
+ }
78
+
79
+ openLightPopup() {
80
+ const slot = this.renderRoot?.querySelector(`slot[name='popup']`) as HTMLSlotElement
81
+ const slottedElements = slot?.assignedElements({ flatten: true })
82
+ const originalContent = slottedElements?.[0] as HTMLElement
83
+
84
+ if (originalContent) {
85
+ this.lightPopupCustomCreator(originalContent)
86
+ } else {
87
+ this.lightPopupRecordView()
88
+ }
89
+ }
90
+
91
+ openPopup() {
92
+ const slot = this.renderRoot?.querySelector(`slot[name='popup']`) as HTMLSlotElement
93
+ const slottedElements = slot?.assignedElements({ flatten: true })
94
+ const originalContent = slottedElements?.[0] as HTMLElement
95
+
96
+ if (originalContent) {
97
+ this.popupCustomCreator(originalContent)
98
+ } else {
99
+ this.popupRecordView()
100
+ }
44
101
  }
45
102
 
46
103
  lightPopupRecordView() {
@@ -87,11 +144,17 @@ export class RecordCreator extends LitElement {
87
144
  @cancel=${(e: Event) => {
88
145
  popup.close()
89
146
  }}
90
- @ok=${(e: Event) => {
91
- popup.close()
92
-
147
+ @ok=${async (e: Event) => {
93
148
  const view = e.currentTarget as RecordView
94
149
 
150
+ const invalidFields = await this.validateRecord(view.record)
151
+ if (invalidFields.length > 0) {
152
+ view.setFocusOnInvalid(invalidFields)
153
+ return false
154
+ }
155
+
156
+ popup.close()
157
+
95
158
  this.dispatchEvent(
96
159
  new CustomEvent('ok', {
97
160
  bubbles: true,
@@ -142,8 +205,17 @@ export class RecordCreator extends LitElement {
142
205
 
143
206
  recordView.addEventListener('ok', async (e: Event) => {
144
207
  const view = e.currentTarget as RecordView
208
+
209
+ const invalidFields = await this.validateRecord(view.record)
210
+ if (invalidFields.length > 0) {
211
+ view.setFocusOnInvalid(invalidFields)
212
+ return false
213
+ }
214
+
145
215
  if (await this.callback?.(view.record)) {
146
216
  popup.close()
217
+ } else {
218
+ console.error('validation failed')
147
219
  }
148
220
  })
149
221
 
@@ -177,4 +249,41 @@ export class RecordCreator extends LitElement {
177
249
  })
178
250
  )
179
251
  }
252
+
253
+ lightPopupCustomCreator(originalContent: HTMLElement) {
254
+ const title = 'create'
255
+ const popupContent = originalContent.cloneNode(true) as HTMLElement
256
+ popupContent.removeAttribute('slot')
257
+
258
+ OxPopup.open({
259
+ template: html`
260
+ <div title>${title}</div>
261
+ ${popupContent}
262
+ `,
263
+ parent: document.body,
264
+ preventCloseOnBlur: this.preventCloseOnBlur
265
+ })
266
+
267
+ this.customPopupCallback?.(popupContent) // 사용자 정의 팝업용 콜백 실행
268
+ }
269
+
270
+ popupCustomCreator(originalContent: HTMLElement) {
271
+ const title = 'create'
272
+ const popupContent = originalContent.cloneNode(true) as HTMLElement
273
+ popupContent.removeAttribute('slot')
274
+
275
+ document.dispatchEvent(
276
+ new CustomEvent('open-popup', {
277
+ detail: {
278
+ template: popupContent,
279
+ options: {
280
+ backdrop: true,
281
+ size: 'large',
282
+ title
283
+ },
284
+ callback: this.customPopupCallback
285
+ }
286
+ })
287
+ )
288
+ }
180
289
  }
@@ -5,8 +5,9 @@ import { css, html, LitElement } from 'lit'
5
5
  import { customElement, property } from 'lit/decorators.js'
6
6
 
7
7
  import { ZERO_RECORD } from '../configure/zero-config'
8
- import { ColumnConfig, GristRecord } from '../types'
8
+ import { ColumnConfig, GristRecord, ValidationReason } from '../types'
9
9
  import { recordViewBodyClickHandler } from './event-handlers/record-view-body-click-handler'
10
+ import i18next from 'i18next'
10
11
 
11
12
  @customElement('ox-record-view-body')
12
13
  export class RecordViewBody extends LitElement {
@@ -86,7 +87,21 @@ export class RecordViewBody extends LitElement {
86
87
  color: var(--record-view-focus-color);
87
88
  font-weight: bold;
88
89
  }
89
-
90
+
91
+ .highlight-invalid {
92
+ position: relative;
93
+ padding: var(--spacing-tiny) var(--spacing-small);
94
+ }
95
+
96
+ .highlight-invalid::after {
97
+ content: attr(data-reason); /* 콘텐츠를 동적으로 변경하기 위해 data-reason 속성을 사용 */
98
+ color: red;
99
+ font-size: 12px;
100
+ position: absolute;
101
+ left: 0;
102
+ bottom: -8px; /* 라벨 아래쪽에 메시지를 표시 */
103
+ }
104
+
90
105
  @media only screen and (max-width: 1000px) {
91
106
  div[content] {
92
107
  grid-template-columns: 2fr 3fr;
@@ -142,6 +157,36 @@ export class RecordViewBody extends LitElement {
142
157
  super.disconnectedCallback()
143
158
  this.removeEventListener('keydown', this._onKeyDown)
144
159
  }
160
+
161
+ setFocus(fieldElement: HTMLElement) {
162
+ fieldElement?.dispatchEvent(new CustomEvent('click', { bubbles: true, composed: true }))
163
+ }
164
+
165
+ setFocusOnInvalid(invalidFields: { field: string; reason: ValidationReason }[]) {
166
+ const allLabels = this.renderRoot.querySelectorAll('label')
167
+ allLabels.forEach((label: HTMLLabelElement) => {
168
+ label.classList.remove('highlight-invalid')
169
+ label.removeAttribute('data-reason')
170
+ });
171
+
172
+ // 유효성 검사를 통과하지 못한 필드에 대해 처리
173
+ invalidFields.forEach(({ field, reason }, index) => {
174
+ const labelElement = this.renderRoot.querySelector(`[data-name="${field}"]`) as HTMLLabelElement
175
+ const fieldElement = this.renderRoot.querySelector(`[data-name="${field}"] + ox-grid-field`) as HTMLInputElement
176
+
177
+ // 동적으로 data-reason 속성을 설정하여 메시지를 변경
178
+ if (labelElement) {
179
+ labelElement.classList.add('highlight-invalid');
180
+ labelElement.setAttribute('data-reason', '(' + i18next.t(`text.validation-reason.${reason}`) + ')')
181
+ }
182
+
183
+ // 첫 번째 필드에 포커스 설정
184
+ if (index === 0 && fieldElement) {
185
+ this.setFocus(fieldElement)
186
+ }
187
+ });
188
+ }
189
+
145
190
 
146
191
  _onKeyDown(event: KeyboardEvent) {
147
192
  if (event.key === 'Tab') {
@@ -156,7 +201,8 @@ export class RecordViewBody extends LitElement {
156
201
  }
157
202
 
158
203
  event.preventDefault()
159
- fields[nextIndex]?.dispatchEvent(new CustomEvent('click', { bubbles: true, composed: true }))
204
+ const nextField = fields[nextIndex] as HTMLInputElement
205
+ nextField && this.setFocus(nextField)
160
206
  }
161
207
  }
162
208
 
@@ -176,7 +222,7 @@ export class RecordViewBody extends LitElement {
176
222
  let dirtyFields = record['__dirtyfields__'] || {}
177
223
 
178
224
  return html`
179
- <label ?editable=${editable} ?wide=${wide}>
225
+ <label ?editable=${editable} ?wide=${wide} data-name=${column.name}>
180
226
  <span>${mandatory ? '*' : ''}${this._renderLabel(column)}</span>
181
227
  <md-icon>edit</md-icon>
182
228
  </label>
@@ -4,12 +4,14 @@ import '@operato/input/ox-input-file.js'
4
4
  import '../data-grid/data-grid-field'
5
5
 
6
6
  import { css, html, LitElement } from 'lit'
7
- import { customElement, property } from 'lit/decorators.js'
7
+ import { customElement, property, query } from 'lit/decorators.js'
8
8
 
9
9
  import { ScrollbarStyles } from '@operato/styles'
10
10
 
11
11
  import { ZERO_RECORD } from '../configure/zero-config'
12
- import { ColumnConfig, GristRecord } from '../types'
12
+ import { ColumnConfig, GristRecord, ValidationReason } from '../types'
13
+
14
+ import { RecordViewBody } from './record-view-body'
13
15
 
14
16
  @customElement('ox-record-view')
15
17
  export class RecordView extends LitElement {
@@ -66,6 +68,8 @@ export class RecordView extends LitElement {
66
68
  @property({ type: Object }) record: GristRecord = ZERO_RECORD
67
69
  @property({ type: Number }) rowIndex: number = -1
68
70
 
71
+ @query('ox-record-view-body') body!: RecordViewBody
72
+
69
73
  render() {
70
74
  return html`
71
75
  <ox-record-view-body .columns=${this.columns} .record=${this.record} .rowIndex=${this.rowIndex}>
@@ -82,6 +86,10 @@ export class RecordView extends LitElement {
82
86
  this.setAttribute('tabindex', '0')
83
87
  }
84
88
 
89
+ setFocusOnInvalid(invalidFields: { field: string; reason: ValidationReason }[]) {
90
+ this.body.setFocusOnInvalid(invalidFields)
91
+ }
92
+
85
93
  onReset() {
86
94
  this.focus()
87
95
 
package/src/types.ts CHANGED
@@ -803,3 +803,11 @@ export type FilterPreference = {
803
803
  hidden?: boolean
804
804
  value?: any
805
805
  }
806
+
807
+ export enum ValidationReason {
808
+ MANDATORY = 'MANDATORY',
809
+ FORMAT = 'FORMAT',
810
+ RANGE = 'RANGE',
811
+ UNIQUE = 'UNIQUE'
812
+ }
813
+
@@ -1,7 +1,7 @@
1
1
  import '../src/index.js'
2
2
  import '../src/filters/filters-form.js'
3
3
  import '../src/sorters/sorters-control.js'
4
- import '../src/record-view/record-creator.js'
4
+ import '../src/record-view/ox-record-creator.js'
5
5
  import '@operato/popup/ox-popup-list.js'
6
6
  import '@material/web/icon/icon.js'
7
7