@things-factory/product-ui 4.3.131 → 4.3.138

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.
@@ -298,6 +298,7 @@ class ProductBundleLinkPopup extends localize(i18next)(LitElement) {
298
298
  records:
299
299
  items.map(itm => {
300
300
  return {
301
+ id: itm.id,
301
302
  productSku: itm.product.sku,
302
303
  productName: itm.product.name,
303
304
  productDescription: itm.product.description,
@@ -4,6 +4,7 @@ import '@things-factory/grist-ui'
4
4
 
5
5
  import gql from 'graphql-tag'
6
6
  import { css, html } from 'lit'
7
+ import isEmpty from 'lodash-es/isEmpty'
7
8
 
8
9
  import { getCodeByName } from '@things-factory/code-base'
9
10
  import { i18next, localize } from '@things-factory/i18n-base'
@@ -107,7 +108,7 @@ class ProductBundleList extends localize(i18next)(PageView) {
107
108
  ]
108
109
 
109
110
  this.productGristConfig = {
110
- pagination: { limit: 50, pages: [20, 50, 100, 500, 999] },
111
+ pagination: { limit: 50, pages: [50, 100, 200, 500] },
111
112
  list: { fields: ['sku', 'name', 'description', 'type', 'packingType'] },
112
113
  rows: {
113
114
  selectable: { multiple: true }
@@ -125,7 +126,7 @@ class ProductBundleList extends localize(i18next)(PageView) {
125
126
  {
126
127
  type: 'string',
127
128
  name: 'sku',
128
- header: i18next.t('field.sku'),
129
+ header: `*${i18next.t('field.sku')}`,
129
130
  sortable: false,
130
131
  record: { editable: true },
131
132
  width: 150
@@ -141,7 +142,7 @@ class ProductBundleList extends localize(i18next)(PageView) {
141
142
  {
142
143
  type: 'string',
143
144
  name: 'name',
144
- header: i18next.t('field.name'),
145
+ header: `*${i18next.t('field.name')}`,
145
146
  sortable: false,
146
147
  record: { editable: true },
147
148
  width: 200
@@ -182,7 +183,7 @@ class ProductBundleList extends localize(i18next)(PageView) {
182
183
  {
183
184
  type: 'select',
184
185
  name: 'packingType',
185
- header: i18next.t('field.packing_type'),
186
+ header: `*${i18next.t('field.packing_type')}`,
186
187
  record: {
187
188
  editable: true,
188
189
  options: ['', ...Object.keys(packingType).map(key => packingType[key].name)]
@@ -274,34 +275,71 @@ class ProductBundleList extends localize(i18next)(PageView) {
274
275
  }
275
276
 
276
277
  async _saveProductBundles(patches) {
277
- if (patches && patches.length) {
278
- const response = await client.mutate({
279
- mutation: gql`
280
- mutation updateMultipleProductBundle($patches: [ProductBundlePatch!]!) {
281
- updateMultipleProductBundle(patches: $patches) {
282
- sku
283
- refCode
284
- name
285
- description
286
- type
287
- packingType
288
- status
278
+ try {
279
+ if (patches && patches.length) {
280
+ this._validate()
281
+ const response = await client.mutate({
282
+ mutation: gql`
283
+ mutation updateMultipleProductBundle($patches: [ProductBundlePatch!]!) {
284
+ updateMultipleProductBundle(patches: $patches) {
285
+ sku
286
+ refCode
287
+ name
288
+ description
289
+ type
290
+ packingType
291
+ status
292
+ }
289
293
  }
290
- }
291
- `,
292
- variables: { patches },
293
- context: gqlContext()
294
- })
294
+ `,
295
+ variables: { patches },
296
+ context: gqlContext()
297
+ })
295
298
 
296
- if (!response.errors) {
297
- this.dataGrist.fetch()
298
- this._showToast(i18next.t('text.data_updated_successfully'))
299
+ if (!response.errors) {
300
+ this.dataGrist.fetch()
301
+ this._showToast(i18next.t('text.data_updated_successfully'))
302
+ }
303
+ } else {
304
+ CustomAlert({
305
+ title: i18next.t('text.nothing_changed'),
306
+ text: i18next.t('text.there_is_nothing_to_save')
307
+ })
299
308
  }
300
- } else {
301
- CustomAlert({
302
- title: i18next.t('text.nothing_changed'),
303
- text: i18next.t('text.there_is_nothing_to_save')
304
- })
309
+ } catch (e) {
310
+ this._showToast(e.message)
311
+ }
312
+ }
313
+
314
+ _validate() {
315
+ let errors = []
316
+
317
+ let data = this.dataGrist._data.records.map(itm => {
318
+ itm.error = false
319
+
320
+ if (isEmpty(itm.sku) || '') {
321
+ itm.error = true
322
+ if (!errors.find(err => err.type == 'sku')) errors.push({ type: 'sku', value: 'SKU is required' })
323
+ }
324
+ if (isEmpty(itm.name) || '') {
325
+ itm.error = true
326
+ if (!errors.find(err => err.type == 'name')) errors.push({ type: 'name', value: 'name is required' })
327
+ }
328
+ if (isEmpty(itm.packingType) || '') {
329
+ itm.error = true
330
+ if (!errors.find(err => err.type == 'packingType'))
331
+ errors.push({ type: 'packingType', value: 'packing type is required' })
332
+ }
333
+ return itm
334
+ })
335
+
336
+ if (errors.length > 0) {
337
+ this._data = {
338
+ ...this.dataGrist.dirtyData,
339
+ records: data
340
+ }
341
+
342
+ throw new Error(errors.map(itm => itm.value).join(', '))
305
343
  }
306
344
  }
307
345
 
@@ -36,6 +36,18 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
36
36
  overflow-y: auto;
37
37
  flex: 1;
38
38
  }
39
+ .gtinGrist {
40
+ display: flex;
41
+ flex-direction: column;
42
+ overflow-y: auto;
43
+ padding-left: 5px;
44
+ padding-right: 5px;
45
+ }
46
+ .grist-container {
47
+ overflow-y: hidden;
48
+ display: flex;
49
+ flex: 1;
50
+ }
39
51
  .button-container {
40
52
  padding: 10px 0 12px 0;
41
53
  text-align: center;
@@ -71,18 +83,24 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
71
83
  _searchFields: Array,
72
84
  config: Object,
73
85
  _data: Object,
86
+ _gtin: Object,
74
87
  productId: String,
75
- _gtins: Array
88
+ _selectedSeq: Number
76
89
  }
77
90
  }
78
91
 
79
92
  constructor() {
80
93
  super()
81
- this._gtins = []
94
+ this._selectedSeq = 1
95
+ this._gtin = {}
82
96
  }
83
97
 
84
98
  get dataGrist() {
85
- return this.shadowRoot.querySelector('data-grist')
99
+ return this.shadowRoot.querySelector('data-grist#productDetail')
100
+ }
101
+
102
+ get gtinGrist() {
103
+ return this.shadowRoot.querySelector('data-grist#productBarcode')
86
104
  }
87
105
 
88
106
  get buttonSave() {
@@ -91,13 +109,26 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
91
109
 
92
110
  render() {
93
111
  return html`
94
- <data-grist
95
- .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
96
- .config=${this.config}
97
- .data=${this._data}
98
- .fetchHandler="${this.fetchHandler.bind(this)}"
99
- @field-change="${this._fieldChange.bind(this)}"
100
- ></data-grist>
112
+ <div class="grist-container">
113
+ <data-grist
114
+ id="productDetail"
115
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
116
+ .config=${this.config}
117
+ .data=${this._data}
118
+ .fetchHandler="${this.fetchHandler.bind(this)}"
119
+ @field-change="${this._fieldChange.bind(this)}"
120
+ ></data-grist>
121
+
122
+ <div class="gtinGrist">
123
+ <data-grist
124
+ id="productBarcode"
125
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
126
+ .config=${this.productBarcodeConfig}
127
+ .data=${this._gtin}
128
+ @field-change="${this._gtinFieldChange.bind(this)}"
129
+ ></data-grist>
130
+ </div>
131
+ </div>
101
132
 
102
133
  <div class="button-container">
103
134
  <mwc-button
@@ -131,6 +162,11 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
131
162
  return {
132
163
  emphasized: record.error || false
133
164
  }
165
+ },
166
+ handlers: {
167
+ click: (columns, data, column, record, rowIndex, field) => {
168
+ if (column.icon !== 'clear') this._selectProductDetail(record)
169
+ }
134
170
  }
135
171
  },
136
172
  pagination: { infinite: true },
@@ -145,7 +181,7 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
145
181
  icon: 'clear',
146
182
  handlers: {
147
183
  click: (_columns, _data, _column, record, _rowIndex) => {
148
- if (record) this._removeGtin(record, _rowIndex)
184
+ if (record) this._removeProductDetail(record, _rowIndex)
149
185
  }
150
186
  }
151
187
  },
@@ -167,8 +203,8 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
167
203
  },
168
204
  {
169
205
  type: 'string',
170
- name: 'gtin',
171
- header: i18next.t('field.gtin'),
206
+ name: 'name',
207
+ header: i18next.t('field.name'),
172
208
  record: { editable: true },
173
209
  sortable: true,
174
210
  width: 180
@@ -184,7 +220,7 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
184
220
  {
185
221
  type: 'select',
186
222
  name: 'packingType',
187
- header: i18next.t('field.packing_type'),
223
+ header: `*${i18next.t('field.packing_type')}`,
188
224
  record: {
189
225
  editable: true,
190
226
  options: ['', ...Object.keys(packingType).map(key => packingType[key].name)]
@@ -198,20 +234,20 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
198
234
  editable: true,
199
235
  options: ['', ...Object.keys(uom).map(key => uom[key].name)]
200
236
  },
201
- header: `${i18next.t('field.uom')}`,
237
+ header: `*${i18next.t('field.uom')}`,
202
238
  width: 80
203
239
  },
204
240
  {
205
241
  type: 'float',
206
242
  name: 'uomValue',
207
243
  record: { editable: true, options: { min: 0 } },
208
- header: `${i18next.t('field.uom_value')}`,
244
+ header: `*${i18next.t('field.uom_value')}`,
209
245
  width: 100
210
246
  },
211
247
  {
212
248
  type: 'select',
213
249
  name: 'childProductDetail',
214
- header: i18next.t('field.child_gtin'),
250
+ header: i18next.t('field.child_product_detail'),
215
251
  record: { editable: true, rowOptionField: 'rowOptionProductDetail' },
216
252
  width: 180
217
253
  },
@@ -377,6 +413,44 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
377
413
  }
378
414
  ]
379
415
  }
416
+
417
+ this.productBarcodeConfig = {
418
+ rows: {
419
+ appendable: true,
420
+ classifier: record => {
421
+ return {
422
+ emphasized: record.error || false
423
+ }
424
+ }
425
+ },
426
+ pagination: { infinite: true },
427
+ columns: [
428
+ { type: 'gutter', gutterName: 'sequence' },
429
+ {
430
+ type: 'gutter',
431
+ gutterName: 'button',
432
+ icon: 'clear',
433
+ handlers: {
434
+ click: (_columns, _data, _column, record, _rowIndex) => {
435
+ if (record) {
436
+ const newData = _data.records.filter((_, idx) => idx !== _rowIndex)
437
+ this._gtin = { ...this._gtin, records: newData }
438
+ let targetProductDetailIndex = this._selectedSeq ? this._selectedSeq : this._data.records.length
439
+ this.dataGrist._data.records[targetProductDetailIndex - 1].productBarcodes = this._gtin.records
440
+ }
441
+ }
442
+ }
443
+ },
444
+ {
445
+ type: 'string',
446
+ name: 'gtin',
447
+ header: i18next.t('field.gtin'),
448
+ record: { editable: true },
449
+ sortable: true,
450
+ width: 250
451
+ }
452
+ ]
453
+ }
380
454
  }
381
455
 
382
456
  async fetchHandler({ page, limit, sorters = [] }) {
@@ -395,7 +469,6 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
395
469
  items {
396
470
  id
397
471
  name
398
- gtin
399
472
  refCode
400
473
  product {
401
474
  id
@@ -413,6 +486,10 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
413
486
  name
414
487
  gtin
415
488
  }
489
+ productBarcodes {
490
+ id
491
+ gtin
492
+ }
416
493
  packingSize
417
494
  bundleQty
418
495
  weightUnit
@@ -461,6 +538,12 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
461
538
  total: response.data.productDetails.total || 0,
462
539
  records: data
463
540
  }
541
+
542
+ this._gtin = {
543
+ productDetailId: data[0].id,
544
+ name: data[0].name,
545
+ records: data[0].productBarcodes
546
+ }
464
547
  }
465
548
  } catch (e) {
466
549
  this._showToast(e)
@@ -471,16 +554,18 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
471
554
  try {
472
555
  this.buttonSave.disabled = true
473
556
  this.dataGrist.showSpinner()
474
-
475
- let patches = this.dataGrist._data.records
476
-
477
557
  this._validate()
478
558
 
479
- patches = this.dataGrist._data.records.map(itm => {
559
+ let patches = this.dataGrist._data.records.map(itm => {
480
560
  return {
481
561
  id: itm.id,
482
562
  name: itm.name,
483
- gtin: itm.gtin,
563
+ productBarcodes: itm.productBarcodes.map(itm => {
564
+ return {
565
+ id: itm?.id,
566
+ gtin: itm.gtin
567
+ }
568
+ }),
484
569
  refCode: itm.refCode,
485
570
  isDefault: itm.isDefault,
486
571
  packingType: itm.packingType,
@@ -566,13 +651,34 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
566
651
  errors.push({ type: 'isDefault', value: 'Must have 1 default' })
567
652
  }
568
653
 
569
- let data = this.dataGrist._data.records.map(itm => {
654
+ let names = this.dataGrist._data.records.map(itm => itm.name)
655
+
656
+ if (names.filter((item, index) => names.indexOf(item) !== index && names !== '').length > 0) {
657
+ throw new Error('Name cannot be duplicated')
658
+ }
659
+
660
+ let totalProductBarcodes = []
661
+
662
+ this.dataGrist._data.records.map(itm => {
663
+ itm.productBarcodes.map(itm2 => totalProductBarcodes.push(itm2.gtin))
664
+ })
665
+
666
+ let duplicateProductBarcode = totalProductBarcodes.find(
667
+ (item, index) => totalProductBarcodes.indexOf(item) !== index
668
+ )
669
+
670
+ if (duplicateProductBarcode) {
671
+ throw new Error('Gtin cannot be duplicated')
672
+ }
673
+
674
+ let data = this.dataGrist._data.records.map((itm, index) => {
570
675
  itm.error = false
571
676
 
572
- if (isEmpty(itm.gtin) || '') {
677
+ if (itm.productBarcodes?.length <= 0 || !itm.productBarcodes) {
573
678
  itm.error = true
574
679
  if (!errors.find(err => err.type == 'gtin')) errors.push({ type: 'gtin', value: 'GTIN is required' })
575
680
  }
681
+
576
682
  if (isEmpty(itm.packingType) || '') {
577
683
  itm.error = true
578
684
  if (!errors.find(err => err.type == 'packingType'))
@@ -624,31 +730,7 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
624
730
  })
625
731
  }
626
732
  break
627
- case 'gtin':
628
- if (this.dataGrist._data.records.filter(x => x.gtin == after).length > 1) {
629
- this.dataGrist._data.records[row].gtin = before
630
-
631
- updatedRecords = {
632
- ...this.dataGrist._data
633
- }
634
-
635
- this._showToast({ type: 'error', message: 'Duplicated GTIN' })
636
- break
637
- }
638
-
639
- updatedRecords = {
640
- ...this.dataGrist._data,
641
- records: this._updateRowOption(
642
- this.dataGrist._data.records.map((record, idx) => {
643
- if (!isEmpty(record.childProductDetail) && record.childProductDetail == before) {
644
- record.childProductDetail = after
645
- }
646
- return record
647
- })
648
- )
649
- }
650
733
 
651
- break
652
734
  case 'uomValue':
653
735
  if (isNaN(after)) {
654
736
  updatedRecords.records[row].uomValue = 1
@@ -668,21 +750,71 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
668
750
  }
669
751
 
670
752
  this._data = { ...updatedRecords }
753
+ this._gtin = this._gtin = record?.productBarcodes
754
+ ? {
755
+ id: record?.id,
756
+ name: record?.name,
757
+ records: record.productBarcodes
758
+ }
759
+ : {
760
+ records: []
761
+ }
762
+ }
763
+
764
+ _gtinFieldChange(e) {
765
+ var { after, before, column, record, row } = e.detail
766
+ let targetProductDetailIndex = this._selectedSeq ? this._selectedSeq : this._data.records.length
767
+ if (this.gtinGrist._data.records.filter(x => x.gtin == after).length > 1) {
768
+ this.gtinGrist._data.records[row].gtin = before
769
+
770
+ this._showToast({ type: 'error', message: 'Duplicated GTIN' })
771
+ return
772
+ }
773
+
774
+ if (targetProductDetailIndex) {
775
+ if (!this._selectedSeq && this._data.records[targetProductDetailIndex - 1]?.__dirty__ !== '+') {
776
+ this._gtin = {
777
+ records: []
778
+ }
779
+
780
+ this._showToast({ type: 'error', message: 'Please enter product detail first' })
781
+ return
782
+ }
783
+
784
+ this._gtin.records ? (this._gtin.records[row] = { gtin: after }) : (this._gtin.records = [{ gtin: after }])
785
+ this._data.records[targetProductDetailIndex - 1].productBarcodes = this._gtin.records
786
+ this.dataGrist.___data.records[targetProductDetailIndex - 1].productBarcodes = this._gtin.records
787
+ }
788
+ }
789
+
790
+ _selectProductDetail(record) {
791
+ this._selectedSeq = record?.__seq__
792
+
793
+ this._gtin = record?.productBarcodes
794
+ ? {
795
+ id: record?.id,
796
+ name: record?.name,
797
+ records: record.productBarcodes
798
+ }
799
+ : {
800
+ records: []
801
+ }
671
802
  }
672
803
 
673
- _removeGtin(record, index) {
804
+ _removeProductDetail(record, index) {
805
+ this._selectedSeq = record?.__seq__
806
+
674
807
  let data = [...this.dataGrist._data.records]
808
+ if (data.length == 1) {
809
+ return
810
+ }
675
811
  data.splice(index, 1)
676
812
 
677
813
  data = data.map(itm => {
678
- return { ...itm, childProductDetail: itm.childProductDetail == record.gtin ? '' : itm.childProductDetail }
814
+ return { ...itm, childProductDetail: itm.childProductDetail == record.name ? '' : itm.childProductDetail }
679
815
  })
680
816
 
681
- if (
682
- !data.find(itm => {
683
- itm.isDefault
684
- })
685
- ) {
817
+ if (!data.find(itm => itm.isDefault)) {
686
818
  data[0].isDefault = true
687
819
  }
688
820
 
@@ -690,6 +822,10 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
690
822
  ...this._data,
691
823
  records: [...this._updateRowOption(data)]
692
824
  }
825
+
826
+ this._gtin = {
827
+ records: []
828
+ }
693
829
  }
694
830
 
695
831
  _updateRowOption(data) {
@@ -697,7 +833,7 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
697
833
  data.map(itm => {
698
834
  return {
699
835
  ...itm,
700
- childProductDetail: itm?.childProductDetail?.gtin || itm?.childProductDetail || '',
836
+ childProductDetail: itm?.childProductDetail?.name || itm?.childProductDetail || '',
701
837
  rowOptionProductDetail: {
702
838
  options: [
703
839
  {
@@ -705,11 +841,11 @@ export class ProductDetailsPopup extends localize(i18next)(LitElement) {
705
841
  value: ''
706
842
  },
707
843
  ...data
708
- .filter(child => child.gtin != itm.gtin && !isEmpty(child.gtin))
844
+ .filter(child => child.name != itm.name && !isEmpty(child.name))
709
845
  .map(x => {
710
846
  const rowOption = {
711
- display: x.gtin,
712
- value: x.gtin
847
+ display: x.name,
848
+ value: x.name
713
849
  }
714
850
  return rowOption
715
851
  })
@@ -884,7 +884,7 @@ class ProductList extends localize(i18next)(PageView) {
884
884
  this.dataGrist.fetch()
885
885
  }
886
886
  } catch (error) {
887
- this.showToast(error)
887
+ this.showToast(error.message)
888
888
  }
889
889
  }
890
890
 
@@ -903,7 +903,8 @@ class ProductList extends localize(i18next)(PageView) {
903
903
  maxQty: patch?.maxQty ? parseInt(patch.maxQty) : undefined,
904
904
  packingSize: patch?.packingSize ? parseInt(patch.packingSize) : undefined,
905
905
  expirationPeriod: patch?.expirationPeriod ? parseInt(patch.expirationPeriod) : undefined,
906
- productRef: patch.productRef?.id ? { id: patch.productRef?.id } : undefined
906
+ productRef: patch.productRef?.id ? { id: patch.productRef?.id } : undefined,
907
+ pickingStrategy: patch?.pickingStrategy ? patch.pickingStrategy.toUpperCase() : undefined
907
908
  }
908
909
  })
909
910
 
@@ -926,7 +927,7 @@ class ProductList extends localize(i18next)(PageView) {
926
927
  this.dataGrist.fetch()
927
928
  }
928
929
  } catch (error) {
929
- this.showToast(error)
930
+ this.showToast(error.message)
930
931
  }
931
932
  }
932
933
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/product-ui",
3
- "version": "4.3.131",
3
+ "version": "4.3.138",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "client/index.js",
6
6
  "things-factory": true,
@@ -24,12 +24,12 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@things-factory/form-ui": "^4.3.131",
27
- "@things-factory/grist-ui": "^4.3.131",
27
+ "@things-factory/grist-ui": "^4.3.138",
28
28
  "@things-factory/i18n-base": "^4.3.131",
29
- "@things-factory/import-ui": "^4.3.131",
29
+ "@things-factory/import-ui": "^4.3.138",
30
30
  "@things-factory/layout-base": "^4.3.131",
31
- "@things-factory/product-base": "^4.3.131",
31
+ "@things-factory/product-base": "^4.3.138",
32
32
  "@things-factory/shell": "^4.3.131"
33
33
  },
34
- "gitHead": "3ae054257c32ac18b023913a313f784459581ea3"
34
+ "gitHead": "1d4e0a3ff6b8792c843b9735a28a3f5fb946b144"
35
35
  }
@@ -20,6 +20,7 @@
20
20
  "field.case_weight": "case weight",
21
21
  "field.case_width": "case width",
22
22
  "field.child_gtin": "child GTIN",
23
+ "field.child_product_detail": "child product detail",
23
24
  "field.child_product_ref": "child product ref",
24
25
  "field.child_qty": "child qty",
25
26
  "field.cogs_account_code": "COGS account code",
@@ -76,5 +76,6 @@
76
76
  "title.product_bundle_setting": "[ko]product bundle setting",
77
77
  "title.product_bundle": "[ko]product bundle",
78
78
  "title.product_set": "[ko]product set",
79
- "title.product": "[ko]product"
79
+ "title.product": "[ko]product",
80
+ "field.child_product_detail": "[ko] child product detail"
80
81
  }
@@ -76,5 +76,6 @@
76
76
  "title.product_bundle_setting": "[ms]product bundle setting",
77
77
  "title.product_bundle": "[ms]product bundle",
78
78
  "title.product_set": "[ms]product set",
79
- "title.product": "[ms]product"
79
+ "title.product": "[ms]product",
80
+ "field.child_product_detail": "[ms] child product detail"
80
81
  }
@@ -76,5 +76,6 @@
76
76
  "title.product_bundle_setting": "[zh]product bundle setting",
77
77
  "title.product_bundle": "[zh]product bundle",
78
78
  "title.product_set": "[zh]product set",
79
- "title.product": "[zh]product"
79
+ "title.product": "[zh]product",
80
+ "field.child_product_detail": "[zh] child product detail"
80
81
  }