@homefile/components-v2 2.14.10 → 2.14.12

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.
@@ -3,9 +3,59 @@ import { useState } from 'react';
3
3
  import { t } from 'i18next';
4
4
  import { Image, Flex, Menu, Text } from '@chakra-ui/react';
5
5
  import { SelectButton, SelectList, SelectItem } from '../..';
6
- import { categoryItems, partnerCategoriesIcons, textOptionVariants, } from '../../../helpers';
6
+ import { partnerCategoriesIcons, textOptionVariants, } from '../../../helpers';
7
7
  export const PartnerCategorySelect = ({ onClick, width = '10rem', }) => {
8
8
  const [category, setCategory] = useState(t('partner.categories.all'));
9
+ const categoryItems = [
10
+ {
11
+ name: t('partner.categories.all'),
12
+ _id: 'all',
13
+ },
14
+ {
15
+ name: t('partner.categories.insurance'),
16
+ _id: 'insurance',
17
+ },
18
+ {
19
+ name: t('partner.categories.maintenance'),
20
+ _id: 'maintenance',
21
+ },
22
+ {
23
+ name: t('partner.categories.warranty'),
24
+ _id: 'warranty',
25
+ },
26
+ {
27
+ name: t('partner.categories.construction'),
28
+ _id: 'construction',
29
+ },
30
+ {
31
+ name: t('partner.categories.finance'),
32
+ _id: 'finance',
33
+ },
34
+ {
35
+ name: t('partner.categories.plumbing'),
36
+ _id: 'plumbing',
37
+ },
38
+ {
39
+ name: t('partner.categories.electrical'),
40
+ _id: 'electrical',
41
+ },
42
+ {
43
+ name: t('partner.categories.design'),
44
+ _id: 'design',
45
+ },
46
+ {
47
+ name: t('partner.categories.organization'),
48
+ _id: 'organization',
49
+ },
50
+ {
51
+ name: t('partner.categories.misc'),
52
+ _id: 'misc',
53
+ },
54
+ {
55
+ name: t('partner.categories.other'),
56
+ _id: 'other',
57
+ },
58
+ ];
9
59
  return (_jsxs(Menu, { children: [_jsx(SelectButton, { variant: "primary", selectedValue: category, width: width }), _jsx(SelectList, { children: categoryItems.map(({ _id, name }) => {
10
60
  const handleClick = () => {
11
61
  setCategory(name);
@@ -770,78 +770,70 @@ export const unknownFormMock = {
770
770
  productClass: '',
771
771
  misc: {
772
772
  information: {
773
- description: 'The LG GBB71PZDMN is a premium refrigerator that offers fresh storage solutions with advanced features.',
773
+ description: 'The KitchenAid KRSC503ESS01 is a side-by-side refrigerator that features a sleek, stainless steel design and a variety of advanced features for food preservation.',
774
774
  features: [
775
- 'Inverter Linear Compressor for energy efficiency',
776
- 'Multi-Air Flow cooling system',
777
- 'Frost-free design',
778
- 'Smart Diagnosis technology',
779
- 'LED interior lighting',
775
+ 'External ice and water dispenser',
776
+ 'Humidity-controlled crisper drawers',
777
+ 'Adaptive cooling technology',
778
+ 'LED lighting',
779
+ 'Adjustable shelves',
780
780
  ],
781
781
  },
782
782
  warranty: {
783
- generalWarranty: '2 years',
784
- compressorWarranty: '10 years',
783
+ duration: '1 Year',
784
+ coverage: 'Parts and labor for the entire unit.',
785
785
  },
786
- estimatedYearOfManufacture: '2020',
786
+ estimatedYearOfManufacture: 2018,
787
787
  specifications: {
788
788
  dimensions: {
789
- width: '595 mm',
790
- height: '2010 mm',
791
- depth: '682 mm',
789
+ height: '69.75 inches',
790
+ width: '36 inches',
791
+ depth: '33.375 inches',
792
792
  },
793
793
  capacity: {
794
- totalNetCapacity: '341 liters',
795
- fridgeNetCapacity: '229 liters',
796
- freezerNetCapacity: '112 liters',
797
- },
798
- energyRating: 'A++',
799
- color: 'Silver',
800
- weight: '70 kg',
801
- installationType: 'Freestanding',
794
+ totalCapacity: '25.8 cubic feet',
795
+ refrigeratorCapacity: '16.69 cubic feet',
796
+ freezerCapacity: '9.11 cubic feet',
797
+ },
798
+ energyEfficiency: 'Energy Star certified',
799
+ color: 'Stainless Steel',
800
+ finish: 'Fingerprint resistant',
801
+ iceMaker: 'Yes',
802
+ waterDispenser: 'Yes',
802
803
  },
803
804
  troubleshootingTips: [
804
805
  {
805
806
  issue: 'Refrigerator not cooling',
806
- tips: [
807
- 'Check if the refrigerator is plugged in and receiving power.',
808
- 'Ensure the temperature settings are correct.',
809
- 'Make sure the vents are not blocked.',
810
- 'Check for a faulty compressor or other internal components.',
811
- ],
812
- },
813
- {
814
- issue: 'Water pooling inside the fridge',
815
- tips: [
816
- 'Check the drainage hole for blockages.',
817
- 'Inspect the door seals for proper sealing.',
818
- 'Ensure that food items do not block airflow.',
819
- ],
820
- },
821
- ],
822
- replacementParts: {
823
- compressor: 'LG part number 4924JA0211A',
824
- doorSeal: 'LG part number 4986JJ3001A',
825
- fanMotor: 'LG part number 4681JB1028C',
826
- lighting: 'LG part number 6912JB2004E',
827
- },
828
- recommendedParts: [
829
- {
830
- partName: 'Water Filter',
831
- partNumber: 'EDR1RXD1',
832
- description: 'Refrigerator water filter for cleaner, fresher water.',
807
+ solution: 'Check if the thermostat is set to the recommended temperature and ensure that doors are sealing properly.',
833
808
  },
834
809
  {
835
- partName: 'Ice Maker Kit',
836
- partNumber: 'W10190948A',
837
- description: 'Replacement ice maker assembly for the freezer.',
810
+ issue: 'Water dispenser not working',
811
+ solution: 'Ensure the water supply is connected and the water filter is not clogged.',
838
812
  },
839
813
  {
840
- partName: 'Shelves',
841
- partNumber: 'W10720274',
842
- description: 'Adjustable shelves for refrigerator compartment.',
814
+ issue: 'Ice maker not producing ice',
815
+ solution: 'Make sure the ice maker is turned on and that the water line is not frozen.',
843
816
  },
844
817
  ],
818
+ replacementParts: {
819
+ recommendedParts: [
820
+ {
821
+ partName: 'Water Filter',
822
+ partNumber: 'EDR1RXD1',
823
+ description: 'Refrigerator water filter for cleaner, fresher water.',
824
+ },
825
+ {
826
+ partName: 'Ice Maker Kit',
827
+ partNumber: 'W10190948A',
828
+ description: 'Replacement ice maker assembly for the freezer.',
829
+ },
830
+ {
831
+ partName: 'Shelves',
832
+ partNumber: 'W10720274',
833
+ description: 'Adjustable shelves for refrigerator compartment.',
834
+ },
835
+ ],
836
+ },
845
837
  },
846
838
  createdAt: '2025-04-17T19:26:39.435Z',
847
839
  updatedAt: '2025-04-17T19:26:39.435Z',
@@ -1,9 +1,11 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
2
  const MAX_DEPTH = 10;
3
3
  const FIELDS_TO_IGNORE = ['_id', '__v', 'createdAt', 'updatedAt'];
4
+ const NESTED_RECURSION_DEPTH = 1;
4
5
  export const mapApiObjectToFormFields = ({ obj = {}, depth = 0, labelPrefix = '', } = {}) => {
5
6
  if (depth > MAX_DEPTH)
6
7
  return [];
8
+ // merge misc into root
7
9
  const misc = 'misc' in obj && typeof obj.misc === 'object' && obj.misc !== null
8
10
  ? obj.misc
9
11
  : {};
@@ -14,160 +16,101 @@ export const mapApiObjectToFormFields = ({ obj = {}, depth = 0, labelPrefix = ''
14
16
  .filter(([key]) => !FIELDS_TO_IGNORE.includes(key))
15
17
  .flatMap(([key, value]) => {
16
18
  const currentLabel = `${labelPrefix ? `${labelPrefix} - ` : ''}${capitalize(key)}`;
19
+ // primitives
17
20
  if (typeof value === 'string') {
18
- return [
19
- {
20
- id: uuidv4(),
21
- name: currentLabel,
22
- label: currentLabel,
23
- description: '',
24
- comments: '',
25
- value,
26
- type: 'text',
27
- visible: true,
28
- },
29
- ];
21
+ return [buildField(currentLabel, 'text', value)];
30
22
  }
31
23
  if (typeof value === 'number') {
32
- return [
33
- {
34
- id: uuidv4(),
35
- name: currentLabel,
36
- label: currentLabel,
37
- description: '',
38
- comments: '',
39
- value: String(value),
40
- type: 'number',
41
- visible: true,
42
- },
43
- ];
24
+ return [buildField(currentLabel, 'number', String(value))];
44
25
  }
45
26
  if (typeof value === 'boolean') {
46
- return [
47
- {
48
- id: uuidv4(),
49
- name: currentLabel,
50
- label: currentLabel,
51
- description: '',
52
- comments: '',
53
- value: String(value),
54
- type: 'switch',
55
- visible: true,
56
- },
57
- ];
27
+ return [buildField(currentLabel, 'switch', String(value))];
58
28
  }
29
+ // string array
59
30
  if (Array.isArray(value) && value.every((v) => typeof v === 'string')) {
60
- const htmlValue = value.map((v) => `${v}`).join('<br/>');
61
- return [
62
- {
63
- id: uuidv4(),
64
- name: currentLabel,
65
- label: currentLabel,
66
- description: '',
67
- comments: '',
68
- value: htmlValue,
69
- type: 'textarea',
70
- visible: true,
71
- },
72
- ];
31
+ return [buildField(currentLabel, 'textarea', value.join('<br/>'))];
73
32
  }
33
+ // object array directly
74
34
  if (Array.isArray(value) &&
75
35
  value.every((v) => Object.prototype.toString.call(v) === '[object Object]')) {
76
- const htmlList = value
77
- .map((item) => {
78
- const flat = flattenObject(item);
79
- return Object.entries(flat)
80
- .map(([k, v]) => {
81
- const label = capitalize(k);
82
- return Array.isArray(v) &&
83
- v.every((el) => typeof el === 'string')
84
- ? `<span><strong>${label}:</strong><ul>${v
85
- .map((el) => `<li>${el}</li>`)
86
- .join('')}</ul></span><br/>`
87
- : `<span><strong>${label}:</strong> ${formatHtmlValue(v)}</span><br/>`;
88
- })
89
- .join('');
90
- })
91
- .join('<br/>');
92
- return [
93
- {
94
- id: uuidv4(),
95
- name: currentLabel,
96
- label: currentLabel,
97
- description: '',
98
- comments: '',
99
- value: `<span>${htmlList}</span>`,
100
- type: 'textarea',
101
- visible: true,
102
- },
103
- ];
36
+ return [buildField(currentLabel, 'textarea', renderObjectArray(value))];
104
37
  }
38
+ // object with single array field
105
39
  if (isRecord(value)) {
106
- const flat = flattenObject(value);
107
- const lines = Object.entries(flat)
108
- .map(([k, v]) => {
109
- const label = capitalize(k);
110
- return Array.isArray(v) && v.every((el) => typeof el === 'string')
111
- ? `<span><strong>${label}:</strong><ul>${v
112
- .map((el) => `<li>${el}</li>`)
113
- .join('')}</ul></span></br/>`
114
- : `<span><strong>${label}:</strong> ${formatHtmlValue(v)}</span></br/>`;
115
- })
116
- .join('');
117
- const htmlValue = `<span>${lines}</span>`;
118
- return [
119
- {
120
- id: uuidv4(),
121
- name: currentLabel,
122
- label: currentLabel,
123
- description: '',
124
- comments: '',
125
- value: htmlValue,
126
- type: 'textarea',
127
- visible: true,
128
- },
129
- ];
40
+ const entries = Object.entries(value);
41
+ if (entries.length === 1 &&
42
+ Array.isArray(entries[0][1]) &&
43
+ entries[0][1].every(isRecord)) {
44
+ const [subKey, subArr] = entries[0];
45
+ const label = `${currentLabel} - ${capitalize(subKey)}`;
46
+ return [
47
+ Object.assign(Object.assign({}, buildField(label, 'textarea', renderObjectArray(subArr))), { name: label, label }),
48
+ ];
49
+ }
130
50
  }
131
- return [
132
- {
133
- id: uuidv4(),
134
- name: currentLabel,
135
- label: currentLabel,
136
- description: '',
137
- comments: '',
138
- value: String(value),
139
- type: 'default',
140
- visible: false,
141
- },
142
- ];
51
+ // nested object
52
+ if (isRecord(value)) {
53
+ if (depth >= NESTED_RECURSION_DEPTH) {
54
+ return mapApiObjectToFormFields({
55
+ obj: value,
56
+ depth: depth + 1,
57
+ labelPrefix: currentLabel,
58
+ });
59
+ }
60
+ // shallow object -> flatten
61
+ return [buildField(currentLabel, 'textarea', renderObject(value))];
62
+ }
63
+ // fallback
64
+ return [buildField(currentLabel, 'default', String(value), false)];
143
65
  });
144
66
  };
145
- const capitalize = (str) => {
146
- return str
147
- .replace(/([a-z])([A-Z])/g, '$1 $2')
148
- .replace(/_/g, ' ')
149
- .replace(/\b\w/g, (c) => c.toUpperCase());
67
+ // helpers
68
+ const buildField = (name, type, value, visible = true) => ({
69
+ id: uuidv4(),
70
+ name,
71
+ label: name,
72
+ description: '',
73
+ comments: '',
74
+ value,
75
+ type: type,
76
+ visible,
77
+ });
78
+ const renderObjectArray = (arr) => arr
79
+ .map((item) => `<div>${renderObject(item).replace(/<br\/\>/g, '')}</div>`)
80
+ .join('<br/>');
81
+ const renderObject = (obj) => {
82
+ const flat = flattenObject(obj);
83
+ return ('<div>' +
84
+ Object.entries(flat)
85
+ .map(([k, v]) => {
86
+ const label = capitalize(k);
87
+ if (Array.isArray(v) && v.every((el) => typeof el === 'string')) {
88
+ return `<div><strong>${label}:</strong><ul>${v
89
+ .map((el) => `<li>${el}</li>`)
90
+ .join('')}</ul></div>`;
91
+ }
92
+ return `<div><strong>${label}:</strong> ${formatHtmlValue(v)}</div>`;
93
+ })
94
+ .join('<br/>') +
95
+ '</div>');
150
96
  };
151
- const isRecord = (val) => {
152
- return typeof val === 'object' && val !== null && !Array.isArray(val);
153
- };
154
- const formatHtmlValue = (val) => {
155
- if (Array.isArray(val) && val.every((v) => typeof v === 'string')) {
156
- return val.map((v) => `${v}`).join('<br/>');
97
+ const capitalize = (str) => str
98
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
99
+ .replace(/_/g, ' ')
100
+ .replace(/\b\w/g, (c) => c.toUpperCase());
101
+ const isRecord = (val) => typeof val === 'object' && val !== null && !Array.isArray(val);
102
+ const formatHtmlValue = (val) => Array.isArray(val) && val.every((v) => typeof v === 'string')
103
+ ? val.join('<br/>')
104
+ : String(val);
105
+ const flattenObject = (obj, parentKey = '') => Object.entries(obj).reduce((acc, [key, value]) => {
106
+ const fullKey = parentKey
107
+ ? `${parentKey} - ${capitalize(key)}`
108
+ : capitalize(key);
109
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
110
+ Object.assign(acc, flattenObject(value, fullKey));
157
111
  }
158
- return String(val);
159
- };
160
- const flattenObject = (obj, parentKey = '') => {
161
- return Object.entries(obj).reduce((acc, [key, value]) => {
162
- const fullKey = parentKey
163
- ? `${parentKey} - ${capitalize(key)}`
164
- : capitalize(key);
165
- if (value && typeof value === 'object' && !Array.isArray(value)) {
166
- Object.assign(acc, flattenObject(value, fullKey));
167
- }
168
- else {
169
- acc[fullKey] = value;
170
- }
171
- return acc;
172
- }, {});
173
- };
112
+ else {
113
+ acc[fullKey] = value;
114
+ }
115
+ return acc;
116
+ }, {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homefile/components-v2",
3
- "version": "2.14.10",
3
+ "version": "2.14.12",
4
4
  "author": "Homefile",
5
5
  "license": "UNLICENSED",
6
6
  "typings": "dist/index.d.ts",
@@ -3,17 +3,68 @@ import { t } from 'i18next'
3
3
  import { Image, Flex, Menu, Text } from '@chakra-ui/react'
4
4
  import { SelectButton, SelectList, SelectItem } from '@/components'
5
5
  import {
6
- categoryItems,
7
6
  partnerCategoriesIcons,
8
7
  textOptionVariants,
9
8
  } from '@/helpers'
10
- import { PartnerCategorySelectI } from '@/interfaces'
9
+ import { PartnerCategorySelectI, SelectItemI, PartnerCategoryTypes } from '@/interfaces'
11
10
 
12
11
  export const PartnerCategorySelect = ({
13
12
  onClick,
14
13
  width = '10rem',
15
14
  }: PartnerCategorySelectI) => {
16
15
  const [category, setCategory] = useState<string>(t('partner.categories.all'))
16
+
17
+ const categoryItems: SelectItemI<PartnerCategoryTypes>[] = [
18
+ {
19
+ name: t('partner.categories.all'),
20
+ _id: 'all',
21
+ },
22
+ {
23
+ name: t('partner.categories.insurance'),
24
+ _id: 'insurance',
25
+ },
26
+ {
27
+ name: t('partner.categories.maintenance'),
28
+ _id: 'maintenance',
29
+ },
30
+ {
31
+ name: t('partner.categories.warranty'),
32
+ _id: 'warranty',
33
+ },
34
+ {
35
+ name: t('partner.categories.construction'),
36
+ _id: 'construction',
37
+ },
38
+ {
39
+ name: t('partner.categories.finance'),
40
+ _id: 'finance',
41
+ },
42
+ {
43
+ name: t('partner.categories.plumbing'),
44
+ _id: 'plumbing',
45
+ },
46
+ {
47
+ name: t('partner.categories.electrical'),
48
+ _id: 'electrical',
49
+ },
50
+ {
51
+ name: t('partner.categories.design'),
52
+ _id: 'design',
53
+ },
54
+ {
55
+ name: t('partner.categories.organization'),
56
+ _id: 'organization',
57
+ },
58
+ {
59
+ name: t('partner.categories.misc'),
60
+ _id: 'misc',
61
+ },
62
+ {
63
+ name: t('partner.categories.other'),
64
+ _id: 'other',
65
+ },
66
+ ]
67
+
17
68
  return (
18
69
  <Menu>
19
70
  <SelectButton variant="primary" selectedValue={category} width={width} />
@@ -781,78 +781,73 @@ export const unknownFormMock: Record<string, any> = {
781
781
  misc: {
782
782
  information: {
783
783
  description:
784
- 'The LG GBB71PZDMN is a premium refrigerator that offers fresh storage solutions with advanced features.',
784
+ 'The KitchenAid KRSC503ESS01 is a side-by-side refrigerator that features a sleek, stainless steel design and a variety of advanced features for food preservation.',
785
785
  features: [
786
- 'Inverter Linear Compressor for energy efficiency',
787
- 'Multi-Air Flow cooling system',
788
- 'Frost-free design',
789
- 'Smart Diagnosis technology',
790
- 'LED interior lighting',
786
+ 'External ice and water dispenser',
787
+ 'Humidity-controlled crisper drawers',
788
+ 'Adaptive cooling technology',
789
+ 'LED lighting',
790
+ 'Adjustable shelves',
791
791
  ],
792
792
  },
793
793
  warranty: {
794
- generalWarranty: '2 years',
795
- compressorWarranty: '10 years',
794
+ duration: '1 Year',
795
+ coverage: 'Parts and labor for the entire unit.',
796
796
  },
797
- estimatedYearOfManufacture: '2020',
797
+ estimatedYearOfManufacture: 2018,
798
798
  specifications: {
799
799
  dimensions: {
800
- width: '595 mm',
801
- height: '2010 mm',
802
- depth: '682 mm',
800
+ height: '69.75 inches',
801
+ width: '36 inches',
802
+ depth: '33.375 inches',
803
803
  },
804
804
  capacity: {
805
- totalNetCapacity: '341 liters',
806
- fridgeNetCapacity: '229 liters',
807
- freezerNetCapacity: '112 liters',
808
- },
809
- energyRating: 'A++',
810
- color: 'Silver',
811
- weight: '70 kg',
812
- installationType: 'Freestanding',
805
+ totalCapacity: '25.8 cubic feet',
806
+ refrigeratorCapacity: '16.69 cubic feet',
807
+ freezerCapacity: '9.11 cubic feet',
808
+ },
809
+ energyEfficiency: 'Energy Star certified',
810
+ color: 'Stainless Steel',
811
+ finish: 'Fingerprint resistant',
812
+ iceMaker: 'Yes',
813
+ waterDispenser: 'Yes',
813
814
  },
814
815
  troubleshootingTips: [
815
816
  {
816
817
  issue: 'Refrigerator not cooling',
817
- tips: [
818
- 'Check if the refrigerator is plugged in and receiving power.',
819
- 'Ensure the temperature settings are correct.',
820
- 'Make sure the vents are not blocked.',
821
- 'Check for a faulty compressor or other internal components.',
822
- ],
823
- },
824
- {
825
- issue: 'Water pooling inside the fridge',
826
- tips: [
827
- 'Check the drainage hole for blockages.',
828
- 'Inspect the door seals for proper sealing.',
829
- 'Ensure that food items do not block airflow.',
830
- ],
831
- },
832
- ],
833
- replacementParts: {
834
- compressor: 'LG part number 4924JA0211A',
835
- doorSeal: 'LG part number 4986JJ3001A',
836
- fanMotor: 'LG part number 4681JB1028C',
837
- lighting: 'LG part number 6912JB2004E',
838
- },
839
- recommendedParts: [
840
- {
841
- partName: 'Water Filter',
842
- partNumber: 'EDR1RXD1',
843
- description: 'Refrigerator water filter for cleaner, fresher water.',
818
+ solution:
819
+ 'Check if the thermostat is set to the recommended temperature and ensure that doors are sealing properly.',
844
820
  },
845
821
  {
846
- partName: 'Ice Maker Kit',
847
- partNumber: 'W10190948A',
848
- description: 'Replacement ice maker assembly for the freezer.',
822
+ issue: 'Water dispenser not working',
823
+ solution:
824
+ 'Ensure the water supply is connected and the water filter is not clogged.',
849
825
  },
850
826
  {
851
- partName: 'Shelves',
852
- partNumber: 'W10720274',
853
- description: 'Adjustable shelves for refrigerator compartment.',
827
+ issue: 'Ice maker not producing ice',
828
+ solution:
829
+ 'Make sure the ice maker is turned on and that the water line is not frozen.',
854
830
  },
855
831
  ],
832
+ replacementParts: {
833
+ recommendedParts: [
834
+ {
835
+ partName: 'Water Filter',
836
+ partNumber: 'EDR1RXD1',
837
+ description: 'Refrigerator water filter for cleaner, fresher water.',
838
+ },
839
+ {
840
+ partName: 'Ice Maker Kit',
841
+ partNumber: 'W10190948A',
842
+ description: 'Replacement ice maker assembly for the freezer.',
843
+ },
844
+ {
845
+ partName: 'Shelves',
846
+ partNumber: 'W10720274',
847
+ description: 'Adjustable shelves for refrigerator compartment.',
848
+ },
849
+ ],
850
+ },
856
851
  },
857
852
  createdAt: '2025-04-17T19:26:39.435Z',
858
853
  updatedAt: '2025-04-17T19:26:39.435Z',
@@ -9,6 +9,7 @@ interface MapApiObjectToFormFieldsI {
9
9
 
10
10
  const MAX_DEPTH = 10
11
11
  const FIELDS_TO_IGNORE = ['_id', '__v', 'createdAt', 'updatedAt']
12
+ const NESTED_RECURSION_DEPTH = 1
12
13
 
13
14
  export const mapApiObjectToFormFields = ({
14
15
  obj = {},
@@ -17,191 +18,146 @@ export const mapApiObjectToFormFields = ({
17
18
  }: MapApiObjectToFormFieldsI = {}): ReportI[] => {
18
19
  if (depth > MAX_DEPTH) return []
19
20
 
21
+ // merge misc into root
20
22
  const misc =
21
23
  'misc' in obj && typeof obj.misc === 'object' && obj.misc !== null
22
24
  ? obj.misc
23
25
  : {}
24
26
  const rest = { ...obj }
25
27
  delete rest.misc
26
-
27
28
  const merged = { ...rest, ...misc }
28
29
 
29
30
  return Object.entries(merged)
30
31
  .filter(([key]) => !FIELDS_TO_IGNORE.includes(key))
31
- .flatMap(([key, value]): any => {
32
+ .flatMap(([key, value]): ReportI[] => {
32
33
  const currentLabel = `${
33
34
  labelPrefix ? `${labelPrefix} - ` : ''
34
35
  }${capitalize(key)}`
35
36
 
37
+ // primitives
36
38
  if (typeof value === 'string') {
37
- return [
38
- {
39
- id: uuidv4(),
40
- name: currentLabel,
41
- label: currentLabel,
42
- description: '',
43
- comments: '',
44
- value,
45
- type: 'text',
46
- visible: true,
47
- },
48
- ]
39
+ return [buildField(currentLabel, 'text', value)]
49
40
  }
50
-
51
41
  if (typeof value === 'number') {
52
- return [
53
- {
54
- id: uuidv4(),
55
- name: currentLabel,
56
- label: currentLabel,
57
- description: '',
58
- comments: '',
59
- value: String(value),
60
- type: 'number',
61
- visible: true,
62
- },
63
- ]
42
+ return [buildField(currentLabel, 'number', String(value))]
64
43
  }
65
-
66
44
  if (typeof value === 'boolean') {
67
- return [
68
- {
69
- id: uuidv4(),
70
- name: currentLabel,
71
- label: currentLabel,
72
- description: '',
73
- comments: '',
74
- value: String(value),
75
- type: 'switch',
76
- visible: true,
77
- },
78
- ]
45
+ return [buildField(currentLabel, 'switch', String(value))]
79
46
  }
80
47
 
48
+ // string array
81
49
  if (Array.isArray(value) && value.every((v) => typeof v === 'string')) {
82
- const htmlValue = value.map((v) => `${v}`).join('<br/>')
83
- return [
84
- {
85
- id: uuidv4(),
86
- name: currentLabel,
87
- label: currentLabel,
88
- description: '',
89
- comments: '',
90
- value: htmlValue,
91
- type: 'textarea',
92
- visible: true,
93
- },
94
- ]
50
+ return [buildField(currentLabel, 'textarea', value.join('<br/>'))]
95
51
  }
96
52
 
53
+ // object array directly
97
54
  if (
98
55
  Array.isArray(value) &&
99
56
  value.every(
100
57
  (v) => Object.prototype.toString.call(v) === '[object Object]'
101
58
  )
102
59
  ) {
103
- const htmlList = value
104
- .map((item) => {
105
- const flat = flattenObject(item)
106
- return Object.entries(flat)
107
- .map(([k, v]) => {
108
- const label = capitalize(k)
109
- return Array.isArray(v) &&
110
- v.every((el) => typeof el === 'string')
111
- ? `<span><strong>${label}:</strong><ul>${v
112
- .map((el) => `<li>${el}</li>`)
113
- .join('')}</ul></span><br/>`
114
- : `<span><strong>${label}:</strong> ${formatHtmlValue(
115
- v
116
- )}</span><br/>`
117
- })
118
- .join('')
119
- })
120
- .join('<br/>')
121
-
122
- return [
123
- {
124
- id: uuidv4(),
125
- name: currentLabel,
126
- label: currentLabel,
127
- description: '',
128
- comments: '',
129
- value: `<span>${htmlList}</span>`,
130
- type: 'textarea',
131
- visible: true,
132
- },
133
- ]
60
+ return [buildField(currentLabel, 'textarea', renderObjectArray(value))]
61
+ }
62
+
63
+ // object with single array field
64
+ if (isRecord(value)) {
65
+ const entries = Object.entries(value) as [string, any][]
66
+ if (
67
+ entries.length === 1 &&
68
+ Array.isArray(entries[0][1]) &&
69
+ entries[0][1].every(isRecord)
70
+ ) {
71
+ const [subKey, subArr] = entries[0]
72
+ const label = `${currentLabel} - ${capitalize(subKey)}`
73
+ return [
74
+ {
75
+ ...buildField(label, 'textarea', renderObjectArray(subArr)),
76
+ name: label,
77
+ label,
78
+ },
79
+ ]
80
+ }
134
81
  }
135
82
 
83
+ // nested object
136
84
  if (isRecord(value)) {
137
- const flat = flattenObject(value)
138
-
139
- const lines = Object.entries(flat)
140
- .map(([k, v]) => {
141
- const label = capitalize(k)
142
- return Array.isArray(v) && v.every((el) => typeof el === 'string')
143
- ? `<span><strong>${label}:</strong><ul>${v
144
- .map((el) => `<li>${el}</li>`)
145
- .join('')}</ul></span></br/>`
146
- : `<span><strong>${label}:</strong> ${formatHtmlValue(
147
- v
148
- )}</span></br/>`
85
+ if (depth >= NESTED_RECURSION_DEPTH) {
86
+ return mapApiObjectToFormFields({
87
+ obj: value,
88
+ depth: depth + 1,
89
+ labelPrefix: currentLabel,
149
90
  })
150
- .join('')
151
- const htmlValue = `<span>${lines}</span>`
152
-
153
- return [
154
- {
155
- id: uuidv4(),
156
- name: currentLabel,
157
- label: currentLabel,
158
- description: '',
159
- comments: '',
160
- value: htmlValue,
161
- type: 'textarea',
162
- visible: true,
163
- },
164
- ]
91
+ }
92
+ // shallow object -> flatten
93
+ return [buildField(currentLabel, 'textarea', renderObject(value))]
165
94
  }
166
95
 
167
- return [
168
- {
169
- id: uuidv4(),
170
- name: currentLabel,
171
- label: currentLabel,
172
- description: '',
173
- comments: '',
174
- value: String(value),
175
- type: 'default',
176
- visible: false,
177
- },
178
- ]
96
+ // fallback
97
+ return [buildField(currentLabel, 'default', String(value), false)]
179
98
  })
180
99
  }
181
100
 
182
- const capitalize = (str: string) => {
183
- return str
101
+ // helpers
102
+ const buildField = (
103
+ name: string,
104
+ type: string,
105
+ value: string,
106
+ visible = true
107
+ ): ReportI => ({
108
+ id: uuidv4(),
109
+ name,
110
+ label: name,
111
+ description: '',
112
+ comments: '',
113
+ value,
114
+ type: type as any,
115
+ visible,
116
+ })
117
+
118
+ const renderObjectArray = (arr: Record<string, any>[]): string =>
119
+ arr
120
+ .map((item) => `<div>${renderObject(item).replace(/<br\/\>/g, '')}</div>`)
121
+ .join('<br/>')
122
+
123
+ const renderObject = (obj: Record<string, any>): string => {
124
+ const flat = flattenObject(obj)
125
+ return (
126
+ '<div>' +
127
+ Object.entries(flat)
128
+ .map(([k, v]) => {
129
+ const label = capitalize(k)
130
+ if (Array.isArray(v) && v.every((el) => typeof el === 'string')) {
131
+ return `<div><strong>${label}:</strong><ul>${v
132
+ .map((el) => `<li>${el}</li>`)
133
+ .join('')}</ul></div>`
134
+ }
135
+ return `<div><strong>${label}:</strong> ${formatHtmlValue(v)}</div>`
136
+ })
137
+ .join('<br/>') +
138
+ '</div>'
139
+ )
140
+ }
141
+
142
+ const capitalize = (str: string): string =>
143
+ str
184
144
  .replace(/([a-z])([A-Z])/g, '$1 $2')
185
145
  .replace(/_/g, ' ')
186
146
  .replace(/\b\w/g, (c) => c.toUpperCase())
187
- }
188
147
 
189
- const isRecord = (val: unknown): val is Record<string, any> => {
190
- return typeof val === 'object' && val !== null && !Array.isArray(val)
191
- }
148
+ const isRecord = (val: unknown): val is Record<string, any> =>
149
+ typeof val === 'object' && val !== null && !Array.isArray(val)
192
150
 
193
- const formatHtmlValue = (val: unknown): string => {
194
- if (Array.isArray(val) && val.every((v) => typeof v === 'string')) {
195
- return val.map((v) => `${v}`).join('<br/>')
196
- }
197
- return String(val)
198
- }
151
+ const formatHtmlValue = (val: unknown): string =>
152
+ Array.isArray(val) && val.every((v) => typeof v === 'string')
153
+ ? val.join('<br/>')
154
+ : String(val)
199
155
 
200
156
  const flattenObject = (
201
157
  obj: Record<string, any>,
202
158
  parentKey = ''
203
- ): Record<string, any> => {
204
- return Object.entries(obj).reduce((acc, [key, value]) => {
159
+ ): Record<string, any> =>
160
+ Object.entries(obj).reduce((acc, [key, value]) => {
205
161
  const fullKey = parentKey
206
162
  ? `${parentKey} - ${capitalize(key)}`
207
163
  : capitalize(key)
@@ -212,4 +168,3 @@ const flattenObject = (
212
168
  }
213
169
  return acc
214
170
  }, {} as Record<string, any>)
215
- }