Skip to content

Commit fbc54a5

Browse files
authored
Merge pull request #546 from Lemoncode/feature/#513-create-dialog-import-collection
[TEMPORARY] Feature/#513 create dialog import collection
2 parents d080497 + 1539e59 commit fbc54a5

14 files changed

+872
-355
lines changed

src/App.css

Lines changed: 266 additions & 262 deletions
Large diffs are not rendered by default.

src/common/components/icons/export-icon.component.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ export const ExportIcon = () => {
44
xmlns="http://www.w3.org/2000/svg"
55
width="1.2em"
66
height="1.2em"
7-
viewBox="0 0 256 256"
7+
viewBox="0 0 24 24"
88
>
99
<path
1010
fill="currentColor"
11-
d="M214 112v96a14 14 0 0 1-14 14H56a14 14 0 0 1-14-14v-96a14 14 0 0 1 14-14h24a6 6 0 0 1 0 12H56a2 2 0 0 0-2 2v96a2 2 0 0 0 2 2h144a2 2 0 0 0 2-2v-96a2 2 0 0 0-2-2h-24a6 6 0 0 1 0-12h24a14 14 0 0 1 14 14M92.24 68.24L122 38.49V136a6 6 0 0 0 12 0V38.49l29.76 29.75a6 6 0 1 0 8.48-8.48l-40-40a6 6 0 0 0-8.48 0l-40 40a6 6 0 1 0 8.48 8.48"
11+
d="M5.552 20.968a2.577 2.577 0 0 1-2.5-2.73c-.012-2.153 0-4.306 0-6.459a.5.5 0 0 1 1 0c0 2.2-.032 4.4 0 6.6c.016 1.107.848 1.589 1.838 1.589h12.463A1.55 1.55 0 0 0 19.825 19a3 3 0 0 0 .1-1.061v-6.16a.5.5 0 0 1 1 0c0 2.224.085 4.465 0 6.687a2.567 2.567 0 0 1-2.67 2.5Z"
12+
/>
13+
<path
14+
fill="currentColor"
15+
d="M12.337 3.176a.46.46 0 0 0-.311-.138q-.021.002-.043-.006c-.022-.008-.027 0-.041.006a.46.46 0 0 0-.312.138L7.961 6.845a.5.5 0 0 0 .707.707l2.816-2.815v10.742a.5.5 0 0 0 1 0V4.737L15.3 7.552a.5.5 0 0 0 .707-.707Z"
1216
/>
1317
</svg>
1418
);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const ImportIcon = () => {
2+
return (
3+
<svg
4+
xmlns="http://www.w3.org/2000/svg"
5+
width="1.2em"
6+
height="1.2em"
7+
viewBox="0 0 24 24"
8+
>
9+
<path
10+
fill="currentColor"
11+
d="M5.552 20.968a2.577 2.577 0 0 1-2.5-2.73c-.012-2.153 0-4.306 0-6.459a.5.5 0 0 1 1 0c0 2.2-.032 4.4 0 6.6c.016 1.107.848 1.589 1.838 1.589h12.463A1.55 1.55 0 0 0 19.825 19a3 3 0 0 0 .1-1.061v-6.16a.5.5 0 0 1 1 0c0 2.224.085 4.465 0 6.687a2.567 2.567 0 0 1-2.67 2.5Z"
12+
/>
13+
<path
14+
fill="currentColor"
15+
d="M12.337 15.824a.46.46 0 0 1-.311.138q-.021-.002-.043.006c-.022.008-.027 0-.041-.006a.46.46 0 0 1-.312-.138l-3.669-3.669a.5.5 0 0 1 .707-.707l2.816 2.815V3.515a.5.5 0 0 1 1 0v10.742l2.816-2.815a.5.5 0 1 1 .707.707Z"
16+
/>
17+
</svg>
18+
);
19+
};

src/common/components/modal-dialog/modal-dialog.const.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export const ADD_COLLECTION_TITLE = 'Add Collection';
55
export const EDIT_COLLECTION_TITLE = 'Edit Collection';
66
export const ABOUT_TITLE = 'About us';
77
export const EXPORT_MODEL_TITLE = 'Export Model';
8+
export const IMPORT_COLLECTION_TITLE = 'Import JSON Document';
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { parseJsonToFieldVm } from './import-panel.business';
2+
3+
describe('parseJsonToFieldVm', () => {
4+
it('should return an empty array for an empty object', () => {
5+
// Arrange
6+
const input = {};
7+
8+
// Act
9+
const result = parseJsonToFieldVm(input);
10+
11+
// Assert
12+
expect(result).toEqual([]);
13+
});
14+
15+
it('should parse a simple object with a string field', () => {
16+
// Arrange
17+
const input = { name: 'prueba' };
18+
19+
// Act
20+
const result = parseJsonToFieldVm(input);
21+
22+
// Assert
23+
expect(result).toEqual([
24+
{ id: expect.any(String), PK: false, FK: false, name: 'name', type: 'string', isArray: false, isNN: false }
25+
]);
26+
});
27+
28+
it('should parse an object with various MongoDB-specific fields', () => {
29+
// Arrange
30+
const input = {
31+
_id: { $oid: '650041fe064ea46de0a96a60' },
32+
search: 'Desayuno',
33+
results: { $numberInt: '10' },
34+
page: { $numberInt: '1' },
35+
pageSize: { $numberInt: '10' },
36+
date: { $date: { $numberLong: '1694515710261' } }
37+
};
38+
39+
// Act
40+
const result = parseJsonToFieldVm(input);
41+
42+
// Assert
43+
expect(result).toEqual(
44+
expect.arrayContaining([
45+
{ id: expect.any(String), PK: true, FK: false, name: '_id', type: 'objectId', isArray: false, isNN: true },
46+
{ id: expect.any(String), PK: false, FK: false, name: 'search', type: 'string', isArray: false, isNN: false },
47+
{ id: expect.any(String), PK: false, FK: false, name: 'results', type: 'int', isArray: false, isNN: false },
48+
{ id: expect.any(String), PK: false, FK: false, name: 'page', type: 'int', isArray: false, isNN: false },
49+
{ id: expect.any(String), PK: false, FK: false, name: 'pageSize', type: 'int', isArray: false, isNN: false },
50+
{ id: expect.any(String), PK: false, FK: false, name: 'date', type: 'date', isArray: false, isNN: false }
51+
])
52+
);
53+
});
54+
55+
it('should parse an object with boolean and URL fields', () => {
56+
// Arrange
57+
const input = {
58+
_id: { $oid: '631ef8d39fa25b8b2668b400' },
59+
description: 'Listado de cartas',
60+
isDirectLanding: true,
61+
name: 'Restaurantes sin gluten Málaga provincia',
62+
urlName: 'restaurantes-sin-gluten-malaga-provincia'
63+
};
64+
65+
// Act
66+
const result = parseJsonToFieldVm(input);
67+
68+
// Assert
69+
expect(result).toEqual(
70+
expect.arrayContaining([
71+
{ id: expect.any(String), PK: true, FK: false, name: '_id', type: 'objectId', isArray: false, isNN: true },
72+
{ id: expect.any(String), PK: false, FK: false, name: 'description', type: 'string', isArray: false, isNN: false },
73+
{ id: expect.any(String), PK: false, FK: false, name: 'isDirectLanding', type: 'bool', isArray: false, isNN: false },
74+
{ id: expect.any(String), PK: false, FK: false, name: 'name', type: 'string', isArray: false, isNN: false },
75+
{ id: expect.any(String), PK: false, FK: false, name: 'urlName', type: 'string', isArray: false, isNN: false }
76+
])
77+
);
78+
});
79+
80+
it('should parse an object with nested fields', () => {
81+
// Arrange
82+
const input = {
83+
user: {
84+
id: { $oid: '631ef8d39fa25b8b2668b400' },
85+
name: 'John Doe',
86+
address: {
87+
city: 'Madrid',
88+
zip: { $numberInt: '28001' }
89+
}
90+
}
91+
};
92+
93+
// Act
94+
const result = parseJsonToFieldVm(input);
95+
96+
// Assert
97+
expect(result).toEqual([
98+
{
99+
id: expect.any(String),
100+
PK: false,
101+
FK: false,
102+
name: 'user',
103+
type: 'object',
104+
isArray: false,
105+
isNN: false,
106+
children: expect.arrayContaining([
107+
{ id: expect.any(String), PK: false, FK: false, name: 'id', type: 'objectId', isArray: false, isNN: false },
108+
{ id: expect.any(String), PK: false, FK: false, name: 'name', type: 'string', isArray: false, isNN: false },
109+
{
110+
id: expect.any(String),
111+
PK: false,
112+
FK: false,
113+
name: 'address',
114+
type: 'object',
115+
isArray: false,
116+
isNN: false,
117+
children: expect.arrayContaining([
118+
{ id: expect.any(String), PK: false, FK: false, name: 'city', type: 'string', isArray: false, isNN: false },
119+
{ id: expect.any(String), PK: false, FK: false, name: 'zip', type: 'int', isArray: false, isNN: false }
120+
])
121+
}
122+
])
123+
}
124+
]);
125+
});
126+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { GenerateGUID } from '@/core/model';
2+
import { FieldType, FieldVm } from './import-panel.model';
3+
4+
function inferMongoType(value: object | string | number | boolean | null | undefined): FieldType {
5+
if (value === null) return 'null';
6+
if (value === undefined) return 'undefined';
7+
if (Array.isArray(value)) return 'array';
8+
9+
if (typeof value === 'object') {
10+
if ('$oid' in value) return 'objectId';
11+
if ('$date' in value) return 'date';
12+
if ('$numberInt' in value) return 'int';
13+
if ('$numberLong' in value) return 'long';
14+
if ('$numberDouble' in value) return 'double';
15+
if ('$numberDecimal' in value) return 'decimal';
16+
if ('$regex' in value) return 'regex';
17+
if ('$timestamp' in value) return 'timestamp';
18+
return 'object';
19+
}
20+
21+
switch (typeof value) {
22+
case 'string': return 'string';
23+
case 'number': return 'double';
24+
case 'boolean': return 'bool';
25+
case 'symbol': return 'symbol';
26+
case 'function': return 'javascript';
27+
default: return 'any';
28+
}
29+
}
30+
31+
export function parseJsonToFieldVm(obj: Record<string, unknown>): FieldVm[] {
32+
const result: FieldVm[] = [];
33+
34+
for (const key of Object.keys(obj)) {
35+
const value = obj[key];
36+
const isArray = Array.isArray(value);
37+
const baseValue = isArray ? value[0] : value;
38+
39+
const type = inferMongoType(baseValue);
40+
41+
// Check if the field has isNN in the value or if it is _id
42+
const isNN = isObjectWithIsNN(value) ? value.isNN === true : key === '_id';
43+
44+
const field: FieldVm = {
45+
id: GenerateGUID(),
46+
PK: key === '_id',
47+
FK: false,
48+
name: key,
49+
type,
50+
isArray,
51+
isNN,
52+
};
53+
54+
const isPlainObject = (
55+
type === 'object' &&
56+
baseValue &&
57+
typeof baseValue === 'object' &&
58+
!('$oid' in baseValue || '$date' in baseValue || '$numberInt' in baseValue)
59+
);
60+
61+
if (isPlainObject) {
62+
field.children = parseJsonToFieldVm(baseValue);
63+
}
64+
65+
result.push(field);
66+
}
67+
68+
return result;
69+
}
70+
71+
// Function to check if an object has the isNN property
72+
function isObjectWithIsNN(value: unknown): value is { isNN: boolean } {
73+
return typeof value === 'object' && value !== null && 'isNN' in value;
74+
}
75+
76+
export const validateJsonSchema = (jsonString: string): string | null => {
77+
try {
78+
const parsed = JSON.parse(jsonString);
79+
80+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
81+
return 'Current version only accepts a single document object';
82+
}
83+
84+
if (Array.isArray(parsed) && parsed.length === 1 && typeof parsed[0] === 'object' && Object.keys(parsed[0]).length === 0) {
85+
return 'The JSON is not valid';
86+
}
87+
88+
return null;
89+
} catch {
90+
return 'The JSON is not valid';
91+
}
92+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export type FieldType =
2+
| 'any'
3+
| 'array'
4+
| 'binData'
5+
| 'bool'
6+
| 'date'
7+
| 'dbPointer'
8+
| 'decimal'
9+
| 'double'
10+
| 'enum'
11+
| 'int'
12+
| 'javascript'
13+
| 'long'
14+
| 'maxKey'
15+
| 'minKey'
16+
| 'null'
17+
| 'object'
18+
| 'objectId'
19+
| 'regex'
20+
| 'string'
21+
| 'symbol'
22+
| 'timestamp'
23+
| 'undefined';
24+
25+
export interface FieldVm {
26+
id: string;
27+
PK: boolean;
28+
FK: boolean;
29+
name: string;
30+
type: FieldType;
31+
children?: FieldVm[];
32+
isCollapsed?: boolean;
33+
isArray?: boolean;
34+
isNN?: boolean;
35+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
.table-name,
2+
.json-textarea {
3+
background-color: var(--background-dialog);
4+
position: sticky;
5+
top: 3rem;
6+
left: 0;
7+
padding-top: var(--space-md);
8+
padding-bottom: var(--space-md);
9+
z-index: 2;
10+
text-align: left;
11+
display: flex;
12+
align-items: center;
13+
gap: 10px;
14+
}
15+
16+
.table-name {
17+
border-bottom: 0.5px solid var(--primary-border-color);
18+
}
19+
20+
.table-name label,
21+
.json-textarea label {
22+
display: flex;
23+
align-items: center;
24+
gap: 10px;
25+
width: 100%;
26+
}
27+
28+
.table-name input {
29+
flex: 1;
30+
max-width: 190px;
31+
padding: 8px;
32+
height: 40px;
33+
}
34+
35+
36+
.json-textarea textarea {
37+
flex: 1;
38+
padding: 8px;
39+
height: 40px;
40+
}
41+
42+
.json-textarea textarea {
43+
height: 200px;
44+
resize: vertical;
45+
}

0 commit comments

Comments
 (0)