diff --git a/src/App.css b/src/App.css index bc62be55..edaec624 100644 --- a/src/App.css +++ b/src/App.css @@ -1,356 +1,360 @@ *, *:after, *:before { - box-sizing: border-box; + box-sizing: border-box; } :root { - --padding-table: 38px; - --relation-color: #ecad5a; - --border-radius-table: var(--border-radius-s); - --checkbox-size: 22px; - --space-cells: 9px; - --color-error: rgb(247, 44, 44); - --border-toolbar: 1.5px solid var(--primary-700); - - /* spacing */ - --space-unit: 4px; - --space-xxs: calc(var(--space-unit) / 2); - --space-xs: calc(var(--space-unit) * 2); - --space-sm: calc(var(--space-unit) * 3); - --space-md: calc(var(--space-unit) * 4); - --space-lg: calc(var(--space-unit) * 6); - --space-xl: calc(var(--space-unit) * 8); - --space-xxl: calc(var(--space-unit) * 12); - --space-xxxl: calc(var(--space-unit) * 16); - - /* colors */ - --primary-50: #eef9f1; - --primary-100: #dcf2e3; - --primary-200: #b9e5c7; - --primary-300: #96d9ab; - --primary-400: #73cc8f; - --primary-500: #50bf73; - --primary-600: #40995c; - --primary-700: #307345; - --primary-800: #204c2e; - --primary-900: #102617; - - --secondary-50: #fbf5ed; - --secondary-100: #f7eada; - --secondary-200: #f0d5b5; - --secondary-300: #e8c091; - --secondary-400: #e1ab6c; - --secondary-500: #d99647; - --secondary-600: #ae7839; - --secondary-700: #825a2b; - --secondary-800: #573c1c; - --secondary-900: #2b1e0e; - - /* text */ - --text-color: #f3eded; - --text-disabled: #919191; - --text-dark: #202020; - - /* background */ - --background-50: #e2e7eb; - --background-100: #cad3dc; - --background-200: #a2b3c2; - --background-300: #7b8fa1; - --background-400: #455a6c; - --background-500: #2b3c50; - --background-600: #243446; - --background-700: #1c2d3f; - --background-800: #142231; - --background-900: #0f1924; - - /*font-sizes*/ - --fs-xs: 12px; - --fs-s: 14px; - --fs-m: 16px; - --fs-md: 18px; - --fs-l: 20px; - --fs-xl: 24px; - --fs-xxl: 32px; - - /*font-weight*/ - --fw-light: 300; - --fw-regular: 400; - --fw-medium: 500; - --fw-bold: 600; - --fw-extrabold: 700; - - /*border-radius*/ - --border-radius-unit: 4px; - --border-radius-xxs: calc(var(--border-radius-unit) / 2); - --border-radius-xs: calc(var(--border-radius-unit) * 2); - --border-radius-s: calc(var(--border-radius-unit) * 3); - --border-radius-m: calc(var(--border-radius-unit) * 4); - --border-radius-l: calc(var(--border-radius-unit) * 6); - --border-radius-xl: calc(var(--border-radius-unit) * 8); - --border-radius-xxl: calc(var(--border-radius-unit) * 12); - --border-radius-xxxl: calc(var(--border-radius-unit) * 16); - - --input-border-color: var(--background-400); - --edit-table-header: var(--background-900); - --input-border-color-active: var(--primary-300); - --input-radio-border-color: var(--background-400); - - /* Modal */ - --background-dialog: var(--background-700); - --veil-modal: #0d0d1185; - - /* Canvas */ - --bg-canvas: var(--background-800); - --bg-toolbar: var(--background-400); - --bg-table: var(--background-700); - --header-table: var(--primary-300); - - /* Input */ - --bg-input: var(--background-500); - --bg-input-disabled: var(--background-600); - --hover-input: var(--background-500); - - /* buttons */ - --button-secondary: var(--secondary-400); - --hover-button-secondary: var(--secondary-600); - --hover-button: var(--primary-300); - - /* border */ - --primary-border-color: var(--primary-300); - --secondary-border-color: var(--background-300); - --shadow-filter: var(--primary-600); - - /* checkbox */ - --bg-checkbox: var(--background-500); - --border-checkbox: var(--background-400); - --hover-checkbox: var(--background-200); - - /*About*/ - --color-project: var(--primary-300); - --color-team: var(--secondary-300); - - /* Footer*/ - --footer-background: var(--background-700); - --footer-text-color: var(--primary-300); - - /*Main styles*/ - font-family: Inter, system-ui, Helvetica, Arial, sans-serif; - font-size: var(--fs-m); - line-height: 1.5; - font-weight: var(--fw-regular); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; + --padding-table: 38px; + --relation-color: #ecad5a; + --border-radius-table: var(--border-radius-s); + --checkbox-size: 22px; + --space-cells: 9px; + --color-error: rgb(247, 44, 44); + --border-toolbar: 1.5px solid var(--primary-700); + + /* spacing */ + --space-unit: 4px; + --space-xxs: calc(var(--space-unit) / 2); + --space-xs: calc(var(--space-unit) * 2); + --space-sm: calc(var(--space-unit) * 3); + --space-md: calc(var(--space-unit) * 4); + --space-lg: calc(var(--space-unit) * 6); + --space-xl: calc(var(--space-unit) * 8); + --space-xxl: calc(var(--space-unit) * 12); + --space-xxxl: calc(var(--space-unit) * 16); + + /* colors */ + --primary-50: #eef9f1; + --primary-100: #dcf2e3; + --primary-200: #b9e5c7; + --primary-300: #96d9ab; + --primary-400: #73cc8f; + --primary-500: #50bf73; + --primary-600: #40995c; + --primary-700: #307345; + --primary-800: #204c2e; + --primary-900: #102617; + + --secondary-50: #fbf5ed; + --secondary-100: #f7eada; + --secondary-200: #f0d5b5; + --secondary-300: #e8c091; + --secondary-400: #e1ab6c; + --secondary-500: #d99647; + --secondary-600: #ae7839; + --secondary-700: #825a2b; + --secondary-800: #573c1c; + --secondary-900: #2b1e0e; + + /* text */ + --text-color: #f3eded; + --text-disabled: #919191; + --text-dark: #202020; + + /* background */ + --background-50: #e2e7eb; + --background-100: #cad3dc; + --background-200: #a2b3c2; + --background-300: #7b8fa1; + --background-400: #455a6c; + --background-500: #2b3c50; + --background-600: #243446; + --background-700: #1c2d3f; + --background-800: #142231; + --background-900: #0f1924; + + /*font-sizes*/ + --fs-xs: 12px; + --fs-s: 14px; + --fs-m: 16px; + --fs-md: 18px; + --fs-l: 20px; + --fs-xl: 24px; + --fs-xxl: 32px; + + /*font-weight*/ + --fw-light: 300; + --fw-regular: 400; + --fw-medium: 500; + --fw-bold: 600; + --fw-extrabold: 700; + + /*border-radius*/ + --border-radius-unit: 4px; + --border-radius-xxs: calc(var(--border-radius-unit) / 2); + --border-radius-xs: calc(var(--border-radius-unit) * 2); + --border-radius-s: calc(var(--border-radius-unit) * 3); + --border-radius-m: calc(var(--border-radius-unit) * 4); + --border-radius-l: calc(var(--border-radius-unit) * 6); + --border-radius-xl: calc(var(--border-radius-unit) * 8); + --border-radius-xxl: calc(var(--border-radius-unit) * 12); + --border-radius-xxxl: calc(var(--border-radius-unit) * 16); + + --input-border-color: var(--background-400); + --edit-table-header: var(--background-900); + --input-border-color-active: var(--primary-300); + --input-radio-border-color: var(--background-400); + + /* Modal */ + --background-dialog: var(--background-700); + --veil-modal: #0d0d1185; + + /* Canvas */ + --bg-canvas: var(--background-800); + --bg-toolbar: var(--background-400); + --bg-table: var(--background-700); + --header-table: var(--primary-300); + + /* Input */ + --bg-input: var(--background-500); + --bg-input-disabled: var(--background-600); + --hover-input: var(--background-500); + + /* buttons */ + --button-secondary: var(--secondary-400); + --hover-button-secondary: var(--secondary-600); + --hover-button: var(--primary-300); + + /* border */ + --primary-border-color: var(--primary-300); + --secondary-border-color: var(--background-300); + --shadow-filter: var(--primary-600); + + /* checkbox */ + --bg-checkbox: var(--background-500); + --border-checkbox: var(--background-400); + --hover-checkbox: var(--background-200); + + /*About*/ + --color-project: var(--primary-300); + --color-team: var(--secondary-300); + + /* Footer*/ + --footer-background: var(--background-700); + --footer-text-color: var(--primary-300); + + /*Main styles*/ + font-family: Inter, system-ui, Helvetica, Arial, sans-serif; + font-size: var(--fs-m); + line-height: 1.5; + font-weight: var(--fw-regular); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; } body { - margin: 0; - text-align: center; + margin: 0; + text-align: center; } .light { - /* text */ - --text-color: #202020; - --text-disabled: #aeaeae; - - /* Modal */ - --background-dialog: #f6f7f9; - --veil-modal: #23232385; - - /* Canvas */ - --bg-canvas: #f9fafb; - --bg-table: #f6f7f9; - --header-table: var(--primary-300); - - /* Toolbar */ - --bg-toolbar: #f0f2f5; - --border-toolbar: 1.5px solid var(--primary-200); - - /* Input */ - --bg-input: #f3f5f7; - --bg-input-disabled: #f0f2f5; - --hover-input: #f3f5f7; - --input-border-color: var(--background-200); - --input-border-color-active: var(--primary-500); - --input-radio-border-color: var(--background-200); - - /*Edit-table*/ - --edit-table-header: #e0e6eb; - - /* buttons */ - --button-secondary: var(--secondary-600); - --hover-button-secondary: var(--secondary-600); - --hover-button: var(--primary-300); - - /* border */ - --primary-border-color: var(--primary-400); - --secondary-border-color: #b5b9bc; - --shadow-filter: var(--primary-600); - - /* checkbox */ - --bg-checkbox: #e2e7eb; - - /* Not working*/ - --hover-checkbox: var(--background-300); - - /*About*/ - --color-project: var(--primary-500); - --color-team: var(--secondary-600); - - /* Footer*/ - --footer-background: #f3f5f7; - --footer-text-color: var(--primary-700); + /* text */ + --text-color: #202020; + --text-disabled: #aeaeae; + + /* Modal */ + --background-dialog: #f6f7f9; + --veil-modal: #23232385; + + /* Canvas */ + --bg-canvas: #f9fafb; + --bg-table: #f6f7f9; + --header-table: var(--primary-300); + + /* Toolbar */ + --bg-toolbar: #f0f2f5; + --border-toolbar: 1.5px solid var(--primary-200); + + /* Input */ + --bg-input: #f3f5f7; + --bg-input-disabled: #f0f2f5; + --hover-input: #f3f5f7; + --input-border-color: var(--background-200); + --input-border-color-active: var(--primary-500); + --input-radio-border-color: var(--background-200); + + /*Edit-table*/ + --edit-table-header: #e0e6eb; + + /* buttons */ + --button-secondary: var(--secondary-600); + --hover-button-secondary: var(--secondary-600); + --hover-button: var(--primary-300); + + /* border */ + --primary-border-color: var(--primary-400); + --secondary-border-color: #b5b9bc; + --shadow-filter: var(--primary-600); + + /* checkbox */ + --bg-checkbox: #e2e7eb; + + /* Not working*/ + --hover-checkbox: var(--background-300); + + /*About*/ + --color-project: var(--primary-500); + --color-team: var(--secondary-600); + + /* Footer*/ + --footer-background: #f3f5f7; + --footer-text-color: var(--primary-700); } /* Buttons */ button { - border-radius: var(--border-radius-s); - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: var(--fs-s); - font-weight: var(--fw-medium); - font-family: inherit; - background-color: inherit; - cursor: pointer; - transition: border-color 0.25s; - color: var(--text-color); - transition: all 0.3s ease-in-out; + border-radius: var(--border-radius-s); + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: var(--fs-s); + font-weight: var(--fw-medium); + font-family: inherit; + background-color: inherit; + cursor: pointer; + transition: border-color 0.25s; + color: var(--text-color); + transition: all 0.3s ease-in-out; } button:focus-visible { - outline: 1px auto var(--primary-300); + outline: 1px auto var(--primary-300); } button:hover { - background-color: var(--hover-button); - color: var(--text-dark); + background-color: var(--hover-button); + color: var(--text-dark); } button:disabled { - color: var(--text-disabled); - cursor: default; + color: var(--text-disabled); + cursor: default; } button:disabled:hover { - background: transparent; + background: transparent; } .button-secondary { - background-color: var(--secondary-300); - color: var(--text-dark); - margin-top: var(--space-md); - border-radius: var(--border-radius-s); + background-color: var(--secondary-300); + color: var(--text-dark); + margin-top: var(--space-md); + border-radius: var(--border-radius-s); } .button-secondary:hover { - background-color: var(--hover-button-secondary); + background-color: var(--hover-button-secondary); } .button-tertiary { - background-color: var(--background-500); + background-color: var(--background-500); } .button-tertiary:hover { - background-color: var(--background-300); - color: var(--text-dark); + background-color: var(--background-300); + color: var(--text-dark); } .button-secondary:disabled, .button-tertiary:disabled { - color: var(--text-disabled); - cursor: default; - background-color: var(--background-500); + color: var(--text-disabled); + cursor: default; + background-color: var(--background-500); } .button-secondary:disabled:hover, .button-tertiary:disabled:hover { - background-color: var(--background-500); + background-color: var(--background-500); } .light .button-tertiary { - background-color: var(--background-200); + background-color: var(--background-200); } .light .button-tertiary:hover { - background-color: var(--background-300); + background-color: var(--background-300); } + .light .button-secondary:disabled, .light .button-tertiary:disabled { - background-color: #efefef; - color: #929292; + background-color: #efefef; + color: #929292; } .light .button-secondary:disabled:hover, .light .button-tertiary:disabled:hover { - background-color: #efefef; + background-color: #efefef; } + .two-buttons { - display: flex; - align-items: baseline; - justify-content: center; - margin-top: var(--space-md); - gap: var(--space-lg); + display: flex; + align-items: baseline; + justify-content: center; + margin-top: var(--space-md); + gap: var(--space-lg); } /* Input */ input, -select { - background-color: var(--bg-input); - border-radius: var(--border-radius-xs); - color: var(--text-color); - padding: var(--space-xs); - width: 100%; - border: none; - outline: none; - - border: 1px solid var(--input-border-color); - transition: all 0.2s ease; +select, +textarea { + background-color: var(--bg-input); + border-radius: var(--border-radius-xs); + color: var(--text-color); + padding: var(--space-xs); + width: 100%; + border: none; + outline: none; + + border: 1px solid var(--input-border-color); + transition: all 0.2s ease; } input:focus, select:focus { - border: 1px solid var(--input-border-color-active); - background-color: var(--hover-input); + border: 1px solid var(--input-border-color-active); + background-color: var(--hover-input); } select:hover, input:hover { - background-color: var(--hover-input); - box-shadow: 0 0 4px var(--hover-checkbox); + background-color: var(--hover-input); + box-shadow: 0 0 4px var(--hover-checkbox); } select { - padding: 7px; + padding: 7px; } /* Checkbox */ input[type='checkbox'] { - margin: 0; - width: var(--checkbox-size); - height: var(--checkbox-size); - cursor: pointer; + margin: 0; + width: var(--checkbox-size); + height: var(--checkbox-size); + cursor: pointer; } .light input[type='checkbox'] { - color-scheme: light; + color-scheme: light; } .dark input[type='checkbox'] { - color-scheme: dark; + color-scheme: dark; } .mobile-only { - display: none; + display: none; } @media screen and (max-device-width: 1090px) { - .hide-mobile { - display: none; - } - .mobile-only { - display: block; - } -} + .hide-mobile { + display: none; + } + + .mobile-only { + display: block; + } +} \ No newline at end of file diff --git a/src/common/components/icons/export-icon.component.tsx b/src/common/components/icons/export-icon.component.tsx index ae4b57ae..dd2a216f 100644 --- a/src/common/components/icons/export-icon.component.tsx +++ b/src/common/components/icons/export-icon.component.tsx @@ -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" > + ); diff --git a/src/common/components/icons/import-icon.component.tsx b/src/common/components/icons/import-icon.component.tsx new file mode 100644 index 00000000..8830f759 --- /dev/null +++ b/src/common/components/icons/import-icon.component.tsx @@ -0,0 +1,19 @@ +export const ImportIcon = () => { + return ( + + + + + ); +}; diff --git a/src/common/components/modal-dialog/modal-dialog.const.ts b/src/common/components/modal-dialog/modal-dialog.const.ts index 0a91fde4..df68a4f3 100644 --- a/src/common/components/modal-dialog/modal-dialog.const.ts +++ b/src/common/components/modal-dialog/modal-dialog.const.ts @@ -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'; diff --git a/src/pods/import-collection/import-panel.business.spec.ts b/src/pods/import-collection/import-panel.business.spec.ts new file mode 100644 index 00000000..76af56d3 --- /dev/null +++ b/src/pods/import-collection/import-panel.business.spec.ts @@ -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 } + ]) + } + ]) + } + ]); + }); +}); diff --git a/src/pods/import-collection/import-panel.business.ts b/src/pods/import-collection/import-panel.business.ts new file mode 100644 index 00000000..8c47bbd0 --- /dev/null +++ b/src/pods/import-collection/import-panel.business.ts @@ -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): 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'; + } +}; diff --git a/src/pods/import-collection/import-panel.model.ts b/src/pods/import-collection/import-panel.model.ts new file mode 100644 index 00000000..db1e77fa --- /dev/null +++ b/src/pods/import-collection/import-panel.model.ts @@ -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; +} \ No newline at end of file diff --git a/src/pods/import-collection/import-panel.pod.module.css b/src/pods/import-collection/import-panel.pod.module.css new file mode 100644 index 00000000..869a9488 --- /dev/null +++ b/src/pods/import-collection/import-panel.pod.module.css @@ -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; +} \ No newline at end of file diff --git a/src/pods/import-collection/import-panel.pod.tsx b/src/pods/import-collection/import-panel.pod.tsx new file mode 100644 index 00000000..c0241035 --- /dev/null +++ b/src/pods/import-collection/import-panel.pod.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import classes from './import-panel.pod.module.css'; +import * as canvasVm from '@/core/providers/canvas-schema'; +import * as editTableVm from '../edit-table/edit-table.vm'; +import { doMapOrCreateTable } from '../edit-table/edit-table.business'; +import { mapEditTableVmToTableVm } from '../edit-table/edit-table.mapper'; +import { + parseJsonToFieldVm, + validateJsonSchema, +} from './import-panel.business'; + +interface ImportPanelProps { + table?: canvasVm.TableVm; + relations: canvasVm.RelationVm[]; + onSave: (table: canvasVm.TableVm) => void; + onClose: () => void; +} + +const defaultJson = JSON.stringify( + { + _id: { $oid: '1234567890abcdef12345678' }, + user: { + name: 'Ada Lovelace', + age: { $numberInt: '36' }, + premiumUser: true, + email: 'ada@babbage.com', + preferences: { + theme: 'dark', + notifications: { email: true, sms: false }, + }, + }, + itemsPurchased: [ + { item: 'Mechanical Keyboard', price: { $numberDouble: '99.99' } }, + { item: 'Gaming Mouse', price: { $numberDouble: '49.99' } }, + ], + createdAt: { $date: { $numberLong: '1740499516555' }, isNN: true }, + version: { $numberInt: '1' }, + }, + null, + 2 +); + +export const ImportPanel: React.FC = props => { + const { onSave, onClose, relations, table } = props; + const [jsonContent, setJsonContent] = React.useState(defaultJson); + const [jsonError, setJsonError] = React.useState(null); + + const [editTable, setEditTable] = React.useState(() => + doMapOrCreateTable(relations, table) + ); + + const handleSubmit = () => { + try { + const parsedJson = JSON.parse(jsonContent); + const parsedFields = parseJsonToFieldVm(parsedJson); + const newTable: editTableVm.TableVm = { + ...editTable, + fields: parsedFields, + }; + onSave(mapEditTableVmToTableVm(newTable)); + } catch (error) { + setJsonError('The JSON is not valid'); + } + }; + + const handleJsonContentChange = ( + e: React.ChangeEvent + ) => { + const newValue = e.currentTarget.value; + setJsonContent(newValue); + + const validationError = validateJsonSchema(newValue); + setJsonError(validationError); + }; + + const handleChangeTableName = (e: React.ChangeEvent) => { + const tableName: string = e.currentTarget.value; + setEditTable({ ...editTable, tableName }); + }; + + return ( + <> +
+ +
+ +
+