@operato/data-grist 8.0.0-alpha.4 → 8.0.0-alpha.6

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 (34) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/src/data-card/record-card.js +10 -1
  3. package/dist/src/data-card/record-card.js.map +1 -1
  4. package/dist/src/data-grist.d.ts +16 -1
  5. package/dist/src/data-grist.js +53 -20
  6. package/dist/src/data-grist.js.map +1 -1
  7. package/dist/src/data-list/record-partial.js +10 -1
  8. package/dist/src/data-list/record-partial.js.map +1 -1
  9. package/dist/src/record-view/record-creator.d.ts +5 -0
  10. package/dist/src/record-view/record-creator.js +47 -4
  11. package/dist/src/record-view/record-creator.js.map +1 -1
  12. package/dist/src/record-view/record-view-body.d.ts +6 -1
  13. package/dist/src/record-view/record-view-body.js +43 -4
  14. package/dist/src/record-view/record-view-body.js.map +1 -1
  15. package/dist/src/record-view/record-view.d.ts +7 -1
  16. package/dist/src/record-view/record-view.js +7 -1
  17. package/dist/src/record-view/record-view.js.map +1 -1
  18. package/dist/src/types.d.ts +6 -0
  19. package/dist/src/types.js +7 -0
  20. package/dist/src/types.js.map +1 -1
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +3 -3
  23. package/src/data-card/record-card.ts +10 -1
  24. package/src/data-grist.ts +60 -24
  25. package/src/data-list/record-partial.ts +10 -1
  26. package/src/record-view/record-creator.ts +50 -4
  27. package/src/record-view/record-view-body.ts +50 -4
  28. package/src/record-view/record-view.ts +10 -2
  29. package/src/types.ts +8 -0
  30. package/translations/en.json +5 -1
  31. package/translations/ja.json +5 -1
  32. package/translations/ko.json +5 -1
  33. package/translations/ms.json +5 -1
  34. package/translations/zh.json +5 -1
@@ -25,7 +25,16 @@ const OPTIONS: Intl.DateTimeFormatOptions = {
25
25
  // timeZone: 'America/Los_Angeles'
26
26
  }
27
27
 
28
- const formatter = new Intl.DateTimeFormat(navigator.language, OPTIONS)
28
+ function getSafeFormatter(locale: string, options: Intl.DateTimeFormatOptions) {
29
+ try {
30
+ const safeLocale = locale || 'en-US'
31
+ return new Intl.DateTimeFormat(safeLocale, options)
32
+ } catch (e) {
33
+ return new Intl.DateTimeFormat('en-US', options)
34
+ }
35
+ }
36
+
37
+ const formatter = getSafeFormatter(navigator.language, OPTIONS)
29
38
 
30
39
  @customElement('ox-record-card')
31
40
  export class RecordCard extends LitElement {
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
  }
@@ -26,7 +26,16 @@ const OPTIONS: Intl.DateTimeFormatOptions = {
26
26
  // timeZone: 'America/Los_Angeles'
27
27
  }
28
28
 
29
- const formatter = new Intl.DateTimeFormat(navigator.language, OPTIONS)
29
+ function getSafeFormatter(locale: string, options: Intl.DateTimeFormatOptions) {
30
+ try {
31
+ const safeLocale = locale || 'en-US'
32
+ return new Intl.DateTimeFormat(safeLocale, options)
33
+ } catch (e) {
34
+ return new Intl.DateTimeFormat('en-US', options)
35
+ }
36
+ }
37
+
38
+ const formatter = getSafeFormatter(navigator.language, OPTIONS)
30
39
 
31
40
  @customElement('ox-record-partial')
32
41
  export class RecordPartial extends LitElement {
@@ -7,7 +7,7 @@ import { customElement, property, state } from 'lit/decorators.js'
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')
@@ -43,6 +43,23 @@ export class RecordCreator extends LitElement {
43
43
  return html`<slot></slot>`
44
44
  }
45
45
 
46
+ validateRecord(record: GristRecord): { field: string; reason: ValidationReason }[] {
47
+ const columns = this.grist!.compiledConfig.columns;
48
+ const invalidFields: { field: string; reason: ValidationReason }[] = [];
49
+
50
+ columns.forEach(column => {
51
+ if (column.record?.mandatory && (record[column.name] === undefined || record[column.name] === null || record[column.name] === '')) {
52
+ invalidFields.push({
53
+ field: column.name,
54
+ reason: ValidationReason.MANDATORY
55
+ });
56
+ }
57
+ // 여기에 추가적인 유효성 검사 규칙을 구현할 수 있습니다.
58
+ });
59
+
60
+ return invalidFields
61
+ }
62
+
46
63
  lightPopupRecordView() {
47
64
  const config = this.grist!.compiledConfig
48
65
  var title = 'create'
@@ -87,10 +104,21 @@ export class RecordCreator extends LitElement {
87
104
  @cancel=${(e: Event) => {
88
105
  popup.close()
89
106
  }}
90
- @ok=${(e: Event) => {
91
- popup.close()
107
+ @ok=${async (e: Event) => {
108
+ const view = e.currentTarget as RecordView
92
109
 
93
- const view = e.currentTarget as RecordView
110
+ // 레코드 밸리데이션 체크
111
+ const invalidFields = await this.validateRecord(view.record);
112
+ if (invalidFields.length > 0) {
113
+ // const firstInvalidField = invalidFields[0];
114
+ // if (firstInvalidField) {
115
+ // view.setFocus(firstInvalidField.field)
116
+ // }
117
+ view.setFocusOnInvalid(invalidFields);
118
+ return false;
119
+ }
120
+
121
+ popup.close()
94
122
 
95
123
  this.dispatchEvent(
96
124
  new CustomEvent('ok', {
@@ -142,8 +170,26 @@ export class RecordCreator extends LitElement {
142
170
 
143
171
  recordView.addEventListener('ok', async (e: Event) => {
144
172
  const view = e.currentTarget as RecordView
173
+
174
+ // 레코드 밸리데이션 체크
175
+ const invalidFields = await this.validateRecord(view.record);
176
+ if (invalidFields.length > 0) {
177
+ const firstInvalidField = invalidFields[0];
178
+ if (firstInvalidField) {
179
+ const fieldElement = view.renderRoot?.querySelector(`[name="${firstInvalidField}"]`);
180
+ if (fieldElement) {
181
+ (fieldElement as HTMLElement).focus();
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+
145
187
  if (await this.callback?.(view.record)) {
146
188
  popup.close()
189
+ } else {
190
+ // 밸리데이션 실패 시 처리
191
+ console.error('레코드 밸리데이션 실패');
192
+ // 여기에 사용자에게 오류 메시지를 표시하는 로직을 추가할 수 있습니다.
147
193
  }
148
194
  })
149
195
 
@@ -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 {
@@ -95,7 +96,21 @@ export class RecordViewBody extends LitElement {
95
96
  color: var(--record-view-focus-color);
96
97
  font-weight: bold;
97
98
  }
98
-
99
+
100
+ .highlight-invalid {
101
+ position: relative;
102
+ padding: var(--spacing-tiny) var(--spacing-small);
103
+ }
104
+
105
+ .highlight-invalid::after {
106
+ content: attr(data-reason); /* 콘텐츠를 동적으로 변경하기 위해 data-reason 속성을 사용 */
107
+ color: red;
108
+ font-size: 12px;
109
+ position: absolute;
110
+ left: 0;
111
+ bottom: -8px; /* 라벨 아래쪽에 메시지를 표시 */
112
+ }
113
+
99
114
  @media only screen and (max-width: 1000px) {
100
115
  div[content] {
101
116
  grid-template-columns: 2fr 3fr;
@@ -151,6 +166,36 @@ export class RecordViewBody extends LitElement {
151
166
  super.disconnectedCallback()
152
167
  this.removeEventListener('keydown', this._onKeyDown)
153
168
  }
169
+
170
+ setFocus(fieldElement: HTMLElement) {
171
+ fieldElement?.dispatchEvent(new CustomEvent('click', { bubbles: true, composed: true }))
172
+ }
173
+
174
+ setFocusOnInvalid(invalidFields: { field: string; reason: ValidationReason }[]) {
175
+ const allLabels = this.renderRoot.querySelectorAll('label')
176
+ allLabels.forEach((label: HTMLLabelElement) => {
177
+ label.classList.remove('highlight-invalid')
178
+ label.removeAttribute('data-reason')
179
+ });
180
+
181
+ // 유효성 검사를 통과하지 못한 필드에 대해 처리
182
+ invalidFields.forEach(({ field, reason }, index) => {
183
+ const labelElement = this.renderRoot.querySelector(`[data-name="${field}"]`) as HTMLLabelElement
184
+ const fieldElement = this.renderRoot.querySelector(`[data-name="${field}"] + ox-grid-field`) as HTMLInputElement
185
+
186
+ // 동적으로 data-reason 속성을 설정하여 메시지를 변경
187
+ if (labelElement) {
188
+ labelElement.classList.add('highlight-invalid');
189
+ labelElement.setAttribute('data-reason', '(' + i18next.t(`text.validation-reason.${reason}`) + ')')
190
+ }
191
+
192
+ // 첫 번째 필드에 포커스 설정
193
+ if (index === 0 && fieldElement) {
194
+ this.setFocus(fieldElement)
195
+ }
196
+ });
197
+ }
198
+
154
199
 
155
200
  _onKeyDown(event: KeyboardEvent) {
156
201
  if (event.key === 'Tab') {
@@ -165,7 +210,8 @@ export class RecordViewBody extends LitElement {
165
210
  }
166
211
 
167
212
  event.preventDefault()
168
- fields[nextIndex]?.dispatchEvent(new CustomEvent('click', { bubbles: true, composed: true }))
213
+ const nextField = fields[nextIndex] as HTMLInputElement
214
+ nextField && this.setFocus(nextField)
169
215
  }
170
216
  }
171
217
 
@@ -185,7 +231,7 @@ export class RecordViewBody extends LitElement {
185
231
  let dirtyFields = record['__dirtyfields__'] || {}
186
232
 
187
233
  return html`
188
- <label ?editable=${editable} ?wide=${wide}>
234
+ <label ?editable=${editable} ?wide=${wide} data-name=${column.name}>
189
235
  <span>${mandatory ? '*' : ''}${this._renderLabel(column)}</span>
190
236
  <md-icon>edit</md-icon>
191
237
  </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
+
@@ -6,5 +6,9 @@
6
6
  "label.accumulator_avg": "avg",
7
7
  "label.accumulator_count": "cnt",
8
8
  "label.accumulator_max": "min",
9
- "label.accumulator_min": "max"
9
+ "label.accumulator_min": "max",
10
+ "text.validation-reason.MANDATORY": "mandatory",
11
+ "text.validation-reason.FORMAT": "invalid format",
12
+ "text.validation-reason.RANGE": "out of range",
13
+ "text.validation-reason.UNIQUE": "duplicate value"
10
14
  }
@@ -5,5 +5,9 @@
5
5
  "label.accumulator_avg": "平均",
6
6
  "label.accumulator_count": "カウント",
7
7
  "label.accumulator_max": "最小",
8
- "label.accumulator_min": "最大"
8
+ "label.accumulator_min": "最大",
9
+ "text.validation-reason.MANDATORY": "必須",
10
+ "text.validation-reason.FORMAT": "形式が正しくありません",
11
+ "text.validation-reason.RANGE": "範囲外",
12
+ "text.validation-reason.UNIQUE": "重複"
9
13
  }
@@ -5,5 +5,9 @@
5
5
  "label.accumulator_avg": "평균",
6
6
  "label.accumulator_count": "계수",
7
7
  "label.accumulator_max": "최소",
8
- "label.accumulator_min": "최대"
8
+ "label.accumulator_min": "최대",
9
+ "text.validation-reason.MANDATORY": "필수 입력 항목입니다.",
10
+ "text.validation-reason.FORMAT": "잘못된 형식입니다.",
11
+ "text.validation-reason.RANGE": "범위를 벗어났습니다.",
12
+ "text.validation-reason.UNIQUE": "중복된 값입니다."
9
13
  }
@@ -5,5 +5,9 @@
5
5
  "label.accumulator_avg": "Purata",
6
6
  "label.accumulator_count": "Kiraan",
7
7
  "label.accumulator_max": "Minimum",
8
- "label.accumulator_min": "Maksimum"
8
+ "label.accumulator_min": "Maksimum",
9
+ "text.validation-reason.MANDATORY": "wajib",
10
+ "text.validation-reason.FORMAT": "format tidak sah",
11
+ "text.validation-reason.RANGE": "diluar jangka",
12
+ "text.validation-reason.UNIQUE": "nilai duplikat"
9
13
  }
@@ -5,5 +5,9 @@
5
5
  "label.accumulator_avg": "平均",
6
6
  "label.accumulator_count": "计数",
7
7
  "label.accumulator_max": "最小",
8
- "label.accumulator_min": "最大"
8
+ "label.accumulator_min": "最大",
9
+ "text.validation-reason.MANDATORY": "必填",
10
+ "text.validation-reason.FORMAT": "格式不正确",
11
+ "text.validation-reason.RANGE": "超出范围",
12
+ "text.validation-reason.UNIQUE": "重复值"
9
13
  }