Skip to content

[TEMPORARY] Feature/#513 create dialog import collection #546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
528 changes: 266 additions & 262 deletions src/App.css

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions src/common/components/icons/export-icon.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ export const ExportIcon = () => {
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 256 256"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
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"
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"
/>
<path
fill="currentColor"
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"
/>
</svg>
);
Expand Down
19 changes: 19 additions & 0 deletions src/common/components/icons/import-icon.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const ImportIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
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"
/>
<path
fill="currentColor"
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"
/>
</svg>
);
};
1 change: 1 addition & 0 deletions src/common/components/modal-dialog/modal-dialog.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export const ADD_COLLECTION_TITLE = 'Add Collection';
export const EDIT_COLLECTION_TITLE = 'Edit Collection';
export const ABOUT_TITLE = 'About us';
export const EXPORT_MODEL_TITLE = 'Export Model';
export const IMPORT_COLLECTION_TITLE = 'Import JSON Document';
126 changes: 126 additions & 0 deletions src/pods/import-collection/import-panel.business.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { parseJsonToFieldVm } from './import-panel.business';

describe('parseJsonToFieldVm', () => {
it('should return an empty array for an empty object', () => {
// Arrange
const input = {};

// Act
const result = parseJsonToFieldVm(input);

// Assert
expect(result).toEqual([]);
});

it('should parse a simple object with a string field', () => {
// Arrange
const input = { name: 'prueba' };

// Act
const result = parseJsonToFieldVm(input);

// Assert
expect(result).toEqual([
{ id: expect.any(String), PK: false, FK: false, name: 'name', type: 'string', isArray: false, isNN: false }
]);
});

it('should parse an object with various MongoDB-specific fields', () => {
// Arrange
const input = {
_id: { $oid: '650041fe064ea46de0a96a60' },
search: 'Desayuno',
results: { $numberInt: '10' },
page: { $numberInt: '1' },
pageSize: { $numberInt: '10' },
date: { $date: { $numberLong: '1694515710261' } }
};

// Act
const result = parseJsonToFieldVm(input);

// Assert
expect(result).toEqual(
expect.arrayContaining([
{ id: expect.any(String), PK: true, FK: false, name: '_id', type: 'objectId', isArray: false, isNN: true },
{ id: expect.any(String), PK: false, FK: false, name: 'search', type: 'string', isArray: false, isNN: false },
{ id: expect.any(String), PK: false, FK: false, name: 'results', type: 'int', isArray: false, isNN: false },
{ id: expect.any(String), PK: false, FK: false, name: 'page', type: 'int', isArray: false, isNN: false },
{ id: expect.any(String), PK: false, FK: false, name: 'pageSize', type: 'int', isArray: false, isNN: false },
{ id: expect.any(String), PK: false, FK: false, name: 'date', type: 'date', isArray: false, isNN: false }
])
);
});

it('should parse an object with boolean and URL fields', () => {
// Arrange
const input = {
_id: { $oid: '631ef8d39fa25b8b2668b400' },
description: 'Listado de cartas',
isDirectLanding: true,
name: 'Restaurantes sin gluten Málaga provincia',
urlName: 'restaurantes-sin-gluten-malaga-provincia'
};

// Act
const result = parseJsonToFieldVm(input);

// Assert
expect(result).toEqual(
expect.arrayContaining([
{ id: expect.any(String), PK: true, FK: false, name: '_id', type: 'objectId', isArray: false, isNN: true },
{ id: expect.any(String), PK: false, FK: false, name: 'description', type: 'string', isArray: false, isNN: false },
{ id: expect.any(String), PK: false, FK: false, name: 'isDirectLanding', type: 'bool', isArray: false, isNN: false },
{ id: expect.any(String), PK: false, FK: false, name: 'name', type: 'string', isArray: false, isNN: false },
{ id: expect.any(String), PK: false, FK: false, name: 'urlName', type: 'string', isArray: false, isNN: false }
])
);
});

it('should parse an object with nested fields', () => {
// Arrange
const input = {
user: {
id: { $oid: '631ef8d39fa25b8b2668b400' },
name: 'John Doe',
address: {
city: 'Madrid',
zip: { $numberInt: '28001' }
}
}
};

// Act
const result = parseJsonToFieldVm(input);

// Assert
expect(result).toEqual([
{
id: expect.any(String),
PK: false,
FK: false,
name: 'user',
type: 'object',
isArray: false,
isNN: false,
children: expect.arrayContaining([
{ id: expect.any(String), PK: false, FK: false, name: 'id', type: 'objectId', isArray: false, isNN: false },
{ id: expect.any(String), PK: false, FK: false, name: 'name', type: 'string', isArray: false, isNN: false },
{
id: expect.any(String),
PK: false,
FK: false,
name: 'address',
type: 'object',
isArray: false,
isNN: false,
children: expect.arrayContaining([
{ id: expect.any(String), PK: false, FK: false, name: 'city', type: 'string', isArray: false, isNN: false },
{ id: expect.any(String), PK: false, FK: false, name: 'zip', type: 'int', isArray: false, isNN: false }
])
}
])
}
]);
});
});
92 changes: 92 additions & 0 deletions src/pods/import-collection/import-panel.business.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { GenerateGUID } from '@/core/model';
import { FieldType, FieldVm } from './import-panel.model';

function inferMongoType(value: object | string | number | boolean | null | undefined): FieldType {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (Array.isArray(value)) return 'array';

if (typeof value === 'object') {
if ('$oid' in value) return 'objectId';
if ('$date' in value) return 'date';
if ('$numberInt' in value) return 'int';
if ('$numberLong' in value) return 'long';
if ('$numberDouble' in value) return 'double';
if ('$numberDecimal' in value) return 'decimal';
if ('$regex' in value) return 'regex';
if ('$timestamp' in value) return 'timestamp';
return 'object';
}

switch (typeof value) {
case 'string': return 'string';
case 'number': return 'double';
case 'boolean': return 'bool';
case 'symbol': return 'symbol';
case 'function': return 'javascript';
default: return 'any';
}
}

export function parseJsonToFieldVm(obj: Record<string, unknown>): FieldVm[] {
const result: FieldVm[] = [];

for (const key of Object.keys(obj)) {
const value = obj[key];
const isArray = Array.isArray(value);
const baseValue = isArray ? value[0] : value;

const type = inferMongoType(baseValue);

// Check if the field has isNN in the value or if it is _id
const isNN = isObjectWithIsNN(value) ? value.isNN === true : key === '_id';

const field: FieldVm = {
id: GenerateGUID(),
PK: key === '_id',
FK: false,
name: key,
type,
isArray,
isNN,
};

const isPlainObject = (
type === 'object' &&
baseValue &&
typeof baseValue === 'object' &&
!('$oid' in baseValue || '$date' in baseValue || '$numberInt' in baseValue)
);

if (isPlainObject) {
field.children = parseJsonToFieldVm(baseValue);
}

result.push(field);
}

return result;
}

// Function to check if an object has the isNN property
function isObjectWithIsNN(value: unknown): value is { isNN: boolean } {
return typeof value === 'object' && value !== null && 'isNN' in value;
}

export const validateJsonSchema = (jsonString: string): string | null => {
try {
const parsed = JSON.parse(jsonString);

if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
return 'Current version only accepts a single document object';
}

if (Array.isArray(parsed) && parsed.length === 1 && typeof parsed[0] === 'object' && Object.keys(parsed[0]).length === 0) {
return 'The JSON is not valid';
}

return null;
} catch {
return 'The JSON is not valid';
}
};
35 changes: 35 additions & 0 deletions src/pods/import-collection/import-panel.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type FieldType =
| 'any'
| 'array'
| 'binData'
| 'bool'
| 'date'
| 'dbPointer'
| 'decimal'
| 'double'
| 'enum'
| 'int'
| 'javascript'
| 'long'
| 'maxKey'
| 'minKey'
| 'null'
| 'object'
| 'objectId'
| 'regex'
| 'string'
| 'symbol'
| 'timestamp'
| 'undefined';

export interface FieldVm {
id: string;
PK: boolean;
FK: boolean;
name: string;
type: FieldType;
children?: FieldVm[];
isCollapsed?: boolean;
isArray?: boolean;
isNN?: boolean;
}
45 changes: 45 additions & 0 deletions src/pods/import-collection/import-panel.pod.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.table-name,
.json-textarea {
background-color: var(--background-dialog);
position: sticky;
top: 3rem;
left: 0;
padding-top: var(--space-md);
padding-bottom: var(--space-md);
z-index: 2;
text-align: left;
display: flex;
align-items: center;
gap: 10px;
}

.table-name {
border-bottom: 0.5px solid var(--primary-border-color);
}

.table-name label,
.json-textarea label {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}

.table-name input {
flex: 1;
max-width: 190px;
padding: 8px;
height: 40px;
}


.json-textarea textarea {
flex: 1;
padding: 8px;
height: 40px;
}

.json-textarea textarea {
height: 200px;
resize: vertical;
}
Loading