diff --git a/package-lock.json b/package-lock.json index 73c9c072..2097bcd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "boilerplate", "version": "0.0.0", + "hasInstallScript": true, "dependencies": { "@lemoncode/fonk": "^1.5.4", "@lemoncode/fonk-formik": "^4.0.1", diff --git a/src/common/components/icons/index.ts b/src/common/components/icons/index.ts index 342dbd12..d0202466 100644 --- a/src/common/components/icons/index.ts +++ b/src/common/components/icons/index.ts @@ -1,6 +1,7 @@ export * from './dark-icon.component'; export * from './light-icon.component'; export * from './edit-icon.component'; +export * from './key-icon.component'; export * from './canvas-setting-icon.component'; export * from './relation-icon.component'; export * from './zoom-in-icon.component'; diff --git a/src/common/components/icons/key-icon.component.tsx b/src/common/components/icons/key-icon.component.tsx new file mode 100644 index 00000000..94041449 --- /dev/null +++ b/src/common/components/icons/key-icon.component.tsx @@ -0,0 +1,12 @@ +export const KeyIcon = () => { + 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..6ad8231f 100644 --- a/src/common/components/modal-dialog/modal-dialog.const.ts +++ b/src/common/components/modal-dialog/modal-dialog.const.ts @@ -1,5 +1,6 @@ export const CANVAS_SETTINGS_TITLE = 'Canvas Settings'; export const ADD_RELATION_TITLE = 'Add Relation'; +export const MANAGE_INDEX_TITLE = 'Manage Index'; export const EDIT_RELATION_TITLE = 'Edit Relation'; export const ADD_COLLECTION_TITLE = 'Add Collection'; export const EDIT_COLLECTION_TITLE = 'Edit Collection'; diff --git a/src/core/functions.ts b/src/core/functions.ts new file mode 100644 index 00000000..3f2083d2 --- /dev/null +++ b/src/core/functions.ts @@ -0,0 +1,33 @@ +import { IndexField } from './providers'; + +export const isNullOrWhiteSpace = (str?: string) => !str?.trim(); + +export const parseManageIndexFields = (fieldsString?: string): IndexField[] => { + const fields = fieldsString + ?.split(/\s*,\s*/) // Split by commas with spaces + ?.map(field => { + const [name, ...orderParts] = field.trim().split(/\s+/); // Split by one or more spaces + return { name, orderMethod: orderParts.join(' ') }; // Handle multi-word order methods + }); + return fields?.filter(x => !isNullOrWhiteSpace(x.name)) as IndexField[]; +}; + +export const clonify = (input: object): T => { + const str = JSON.stringify(input); + const obj = JSON.parse(str); + return obj as T; +}; + +export const isEqual = ( + a?: string, + b?: string, + ignoreCaseSensivity?: boolean +): boolean => { + ignoreCaseSensivity = ignoreCaseSensivity ?? true; + if (ignoreCaseSensivity) { + a = a?.toLowerCase(); + b = b?.toLowerCase(); + } + if (a === b) return true; + return false; +}; diff --git a/src/core/model/errorHandling.ts b/src/core/model/errorHandling.ts new file mode 100644 index 00000000..41719dae --- /dev/null +++ b/src/core/model/errorHandling.ts @@ -0,0 +1,10 @@ +export interface errorHandling { + errorKey?: string; + errorMessage?: string; + isSuccessful: boolean; +} + +export interface Output { + errorHandling: errorHandling; + data?: T; +} diff --git a/src/core/providers/canvas-schema/canvas-schema-vlatest.model.ts b/src/core/providers/canvas-schema/canvas-schema-vlatest.model.ts index 8dc9188b..7aaf2283 100644 --- a/src/core/providers/canvas-schema/canvas-schema-vlatest.model.ts +++ b/src/core/providers/canvas-schema/canvas-schema-vlatest.model.ts @@ -1,4 +1,5 @@ import { Coords, FieldType, GUID, Size } from '@/core/model'; +import { errorHandling } from '@/core/model/errorHandling'; export interface TableVm { id: string; @@ -6,6 +7,7 @@ export interface TableVm { tableName: string; x: number; // Canvas X Position y: number; // Canvas Y Position + indexes?: IndexVm[]; } export interface FieldVm { @@ -40,6 +42,22 @@ export interface DatabaseSchemaVm { isPristine?: boolean; } +export type OrderMethod = 'Ascending' | 'Descending'; + +export interface IndexField { + name: string; + orderMethod: OrderMethod; +} +export interface IndexVm { + id: string; + name: string; + isUnique: boolean; + sparse: boolean; + fields: IndexField[]; + fieldsString?: string; + partialFilterExpression?: string; +} + export const createDefaultDatabaseSchemaVm = (): DatabaseSchemaVm => ({ version: '0.1', tables: [], @@ -68,8 +86,10 @@ export interface CanvasSchemaContextVm { updateTablePosition: UpdatePositionFn; doFieldToggleCollapse: (tableId: string, fieldId: GUID) => void; updateFullTable: (table: TableVm) => void; + updateFullTableByCheckingIndexes: (table: TableVm) => errorHandling; addTable: (table: TableVm) => void; addRelation: (relation: RelationVm) => void; + addIndexes: (tableId: GUID, indexes: IndexVm[]) => void; doSelectElement: (id: GUID | null) => void; canUndo: () => boolean; canRedo: () => boolean; diff --git a/src/core/providers/canvas-schema/canvas-schema.business.ts b/src/core/providers/canvas-schema/canvas-schema.business.ts index ec255ef2..136dbf1f 100644 --- a/src/core/providers/canvas-schema/canvas-schema.business.ts +++ b/src/core/providers/canvas-schema/canvas-schema.business.ts @@ -1,5 +1,10 @@ import { produce } from 'immer'; -import { FieldVm, RelationVm, TableVm } from './canvas-schema-vlatest.model'; +import { + FieldVm, + IndexVm, + RelationVm, + TableVm, +} from './canvas-schema-vlatest.model'; import { DatabaseSchemaVm } from './canvas-schema-vlatest.model'; import { GUID } from '@/core/model'; @@ -105,3 +110,15 @@ export const updateRelation = ( draft.relations[index] = relation; } }); + +export const updateIndexes = ( + tableId: GUID, + indexes: IndexVm[], + dbSchema: DatabaseSchemaVm +): DatabaseSchemaVm => + produce(dbSchema, draft => { + const tableIndex = draft.tables.findIndex(t => t.id === tableId); + if (tableIndex !== -1) { + draft.tables[tableIndex].indexes = indexes; + } + }); diff --git a/src/core/providers/canvas-schema/canvas-schema.provider.tsx b/src/core/providers/canvas-schema/canvas-schema.provider.tsx index dc5a6ff4..959910b8 100644 --- a/src/core/providers/canvas-schema/canvas-schema.provider.tsx +++ b/src/core/providers/canvas-schema/canvas-schema.provider.tsx @@ -3,6 +3,7 @@ import { produce } from 'immer'; import { CanvasSchemaContext } from './canvas-schema.context'; import { DatabaseSchemaVm, + IndexVm, RelationVm, TableVm, UpdatePositionItemInfo, @@ -19,10 +20,13 @@ import { addNewTable, updateRelation, updateTable, + updateIndexes, } from './canvas-schema.business'; import { useHistoryManager } from '@/common/undo-redo'; import { mapSchemaToLatestVersion } from './canvas-schema.mapper'; import { useStateWithInterceptor } from './canvas-schema.hook'; +import { indexDuplicateNameChecking } from '@/pods/manage-index/manage-index.business'; +import { errorHandling } from '@/core/model/errorHandling'; interface Props { children: React.ReactNode; @@ -60,6 +64,17 @@ export const CanvasSchemaProvider: React.FC = props => { ); }; + const updateFullTableByCheckingIndexes = (table: TableVm): errorHandling => { + const res = indexDuplicateNameChecking(table, canvasSchema); + if (!res.isSuccessful) { + return res; + } + setSchema(prevSchema => + updateTable(table, { ...prevSchema, isPristine: false }) + ); + return res; + }; + // TODO: #56 created to track this // https://github.com/Lemoncode/mongo-modeler/issues/56 const addTable = (table: TableVm) => { @@ -80,6 +95,12 @@ export const CanvasSchemaProvider: React.FC = props => { } }; + const addIndexes = (tableId: GUID, indexes: IndexVm[]) => { + setSchema(prevSchema => + updateIndexes(tableId, indexes, { ...prevSchema, isPristine: false }) + ); + }; + const updateFullRelation = (relationUpdated: RelationVm) => { setSchema(prevSchema => updateRelation(relationUpdated, { ...prevSchema, isPristine: false }) @@ -167,8 +188,10 @@ export const CanvasSchemaProvider: React.FC = props => { updateTablePosition, doFieldToggleCollapse, updateFullTable, + updateFullTableByCheckingIndexes, addTable, addRelation, + addIndexes, doSelectElement, canUndo, canRedo, diff --git a/src/pods/canvas/canvas-svg.component.tsx b/src/pods/canvas/canvas-svg.component.tsx index dabb5240..62220523 100644 --- a/src/pods/canvas/canvas-svg.component.tsx +++ b/src/pods/canvas/canvas-svg.component.tsx @@ -17,6 +17,7 @@ interface Props { onUpdateTablePosition: UpdatePositionFn; onToggleCollapse: (tableId: GUID, fieldId: GUID) => void; onEditTable: (tableInfo: TableVm) => void; + onManageIndex: (tableInfo: TableVm) => void; onEditRelation: (relationId: GUID) => void; onSelectElement: (relationId: GUID | null) => void; isTabletOrMobileDevice: boolean; @@ -31,6 +32,7 @@ export const CanvasSvgComponent: React.FC = props => { onUpdateTablePosition, onToggleCollapse, onEditTable, + onManageIndex, onEditRelation, onSelectElement, isTabletOrMobileDevice, @@ -63,6 +65,7 @@ export const CanvasSvgComponent: React.FC = props => { updatePosition={onUpdateTablePosition} onToggleCollapse={onToggleCollapse} onEditTable={onEditTable} + onManageIndex={onManageIndex} canvasSize={canvasSize} isSelected={canvasSchema.selectedElementId === table.id} selectTable={onSelectElement} diff --git a/src/pods/canvas/canvas.pod.tsx b/src/pods/canvas/canvas.pod.tsx index fec169a2..bedb0453 100644 --- a/src/pods/canvas/canvas.pod.tsx +++ b/src/pods/canvas/canvas.pod.tsx @@ -17,6 +17,7 @@ import { EDIT_COLLECTION_TITLE, ADD_COLLECTION_TITLE, ADD_RELATION_TITLE, + MANAGE_INDEX_TITLE, } from '@/common/components/modal-dialog'; import { CanvasSvgComponent } from './canvas-svg.component'; import { EditRelationPod } from '../edit-relation'; @@ -25,6 +26,8 @@ import { CanvasAccessible } from './components/canvas-accessible'; import useAutosave from '@/core/autosave/autosave.hook'; import { CANVAS_MAX_WIDTH } from '@/core/providers'; import { setOffSetZoomToCoords } from '@/common/helpers/set-off-set-zoom-to-coords.helper'; +import { ManageIndexPod } from '../manage-index'; + const HEIGHT_OFFSET = 200; const BORDER_MARGIN = 40; export const CanvasPod: React.FC = () => { @@ -35,6 +38,7 @@ export const CanvasPod: React.FC = () => { addRelation, updateTablePosition, updateFullTable, + updateFullTableByCheckingIndexes, doFieldToggleCollapse, doSelectElement, updateFullRelation, @@ -119,6 +123,28 @@ export const CanvasPod: React.FC = () => { ); }; + const handleManageIndexSave = (table: TableVm) => { + const res = updateFullTableByCheckingIndexes(table); + console.log(table); + if (!res.isSuccessful) { + alert(res.errorMessage); + return; + } + closeModal(); + }; + + const handleManageIndex = (tableInfo: TableVm) => { + if (isTabletOrMobileDevice) return; + openModal( + , + MANAGE_INDEX_TITLE + ); + }; + const containerRef = React.useRef(null); const handleScroll = () => { @@ -231,7 +257,6 @@ export const CanvasPod: React.FC = () => { document.removeEventListener('keydown', handleKeyDown); }; }, [modalDialog.isOpen, canvasSchema.selectedElementId]); - return (
{ onUpdateTablePosition={updateTablePosition} onToggleCollapse={handleToggleCollapse} onEditTable={handleEditTable} + onManageIndex={handleManageIndex} onEditRelation={handleEditRelation} onSelectElement={onSelectElement} isTabletOrMobileDevice={isTabletOrMobileDevice} diff --git a/src/pods/canvas/components/table/components/database-table-header.component.tsx b/src/pods/canvas/components/table/components/database-table-header.component.tsx index 08b1c5d9..55b2097c 100644 --- a/src/pods/canvas/components/table/components/database-table-header.component.tsx +++ b/src/pods/canvas/components/table/components/database-table-header.component.tsx @@ -1,4 +1,4 @@ -import { Edit } from '@/common/components'; +import { Edit, KeyIcon } from '@/common/components'; import { TABLE_CONST } from '@/core/providers'; import { TruncatedText } from './truncated-text.component'; import { @@ -16,11 +16,13 @@ interface Props { tableName: string; onSelectTable: () => void; isTabletOrMobileDevice: boolean; + onManageIndex: () => void; } export const DatabaseTableHeader: React.FC = props => { const { onEditTable, + onManageIndex, isSelected, tableName, onSelectTable, @@ -34,6 +36,10 @@ export const DatabaseTableHeader: React.FC = props => { e.stopPropagation(); }; + const handleIndexClick = (e: React.MouseEvent) => { + onManageIndex(); + e.stopPropagation(); + }; const handleClick = (e: React.MouseEvent) => { onSelectTable(); e.stopPropagation(); @@ -72,21 +78,38 @@ export const DatabaseTableHeader: React.FC = props => { textClass={classes.tableText} /> {isSelected && !isTabletOrMobileDevice && ( - - + - - + > + + + + + + + + )} {/* Clikable area to select the table or edit it*/} = props => { y="0" width={ isSelected - ? TABLE_CONST.TABLE_WIDTH - PENCIL_ICON_WIDTH + ? TABLE_CONST.TABLE_WIDTH - PENCIL_ICON_WIDTH - 30 : TABLE_CONST.TABLE_WIDTH } height={TABLE_CONST.HEADER_HEIGHT} diff --git a/src/pods/canvas/components/table/database-table.component.tsx b/src/pods/canvas/components/table/database-table.component.tsx index f486e995..a8c1ffab 100644 --- a/src/pods/canvas/components/table/database-table.component.tsx +++ b/src/pods/canvas/components/table/database-table.component.tsx @@ -19,6 +19,7 @@ interface Props { updatePosition: UpdatePositionFn; onToggleCollapse: (tableId: GUID, fieldId: GUID) => void; onEditTable: (tableInfo: TableVm) => void; + onManageIndex: (tableInfo: TableVm) => void; canvasSize: Size; isSelected: boolean; selectTable: (tableId: GUID) => void; @@ -30,6 +31,7 @@ interface Props { export const DatabaseTable: React.FC = ({ tableInfo, onEditTable, + onManageIndex, updatePosition, onToggleCollapse, canvasSize, @@ -81,6 +83,10 @@ export const DatabaseTable: React.FC = ({ onEditTable(tableInfo); }; + const handleManageIndexClick = () => { + onManageIndex(tableInfo); + }; + return ( = ({ { - const relationId = relations.find(relation => relation.id === id); - if (!relationId) { - throw Error(`Relation for ${relationId} is missing`); + const relation = relations.find(relation => relation.id === id); + if (!relation) { + throw Error(`Relation for ${id} is missing`); } - return relationId; + return relation; }; export const createInitialIdValues = ( diff --git a/src/pods/edit-table/edit-table.mapper.ts b/src/pods/edit-table/edit-table.mapper.ts index 55deb15e..801bfca6 100644 --- a/src/pods/edit-table/edit-table.mapper.ts +++ b/src/pods/edit-table/edit-table.mapper.ts @@ -9,7 +9,7 @@ import * as editTableModel from './edit-table.vm'; const extractFieldIDThatIsFk = (tableId: GUID) => (relation: canvasModel.RelationVm): GUID => { - let result: GUID = '-1'; + const result: GUID = '-1'; switch (relation.type) { case '1:1': if ( @@ -44,7 +44,7 @@ const markFKFields = ( fields: canvasModel.FieldVm[], FKFields: GUID[] ): editTableModel.FieldVm[] => { - let result: editTableModel.FieldVm[] = []; + const result: editTableModel.FieldVm[] = []; fields.forEach(field => { const isFK = FKFields.includes(field.id); result.push({ @@ -93,10 +93,10 @@ export const mapTableVmToEditTableVm = ( const mapEditTableFieldsToTableVmFields = ( fields: editTableModel.FieldVm[] ): canvasModel.FieldVm[] => { - let result: canvasModel.FieldVm[] = []; + const result: canvasModel.FieldVm[] = []; fields.forEach(field => { - const { FK, ...editFieldVm } = field; + const { ...editFieldVm } = field; result.push({ ...editFieldVm, children: !field.children diff --git a/src/pods/manage-index/components/commands/command-icon-button.tsx b/src/pods/manage-index/components/commands/command-icon-button.tsx new file mode 100644 index 00000000..8815a151 --- /dev/null +++ b/src/pods/manage-index/components/commands/command-icon-button.tsx @@ -0,0 +1,16 @@ +interface Props { + onClick: () => void; + icon: JSX.Element; + disabled?: boolean; + ariaLabel?: string; +} + +export const CommandIconButton: React.FC = props => { + const { onClick, icon, disabled, ariaLabel } = props; + + return ( + + ); +}; diff --git a/src/pods/manage-index/components/commands/commands.business-first-item.spec.ts b/src/pods/manage-index/components/commands/commands.business-first-item.spec.ts new file mode 100644 index 00000000..21786274 --- /dev/null +++ b/src/pods/manage-index/components/commands/commands.business-first-item.spec.ts @@ -0,0 +1,43 @@ +import { FieldVm } from '../../manage-index.vm'; +import { isFirstItemInArray } from './commands.business'; + +describe('isFirstItemInArray', () => { + it('should return true when it is the first item of the array', () => { + // Arrange + const indexes: FieldVm[] = [ + { id: '1', name: 'Index1', isUnique: false, sparse: false, fields: [] }, + { id: '2', name: 'Index2', isUnique: false, sparse: false, fields: [] }, + ]; + //Act + const firstItem = isFirstItemInArray(indexes, '1'); + + //Assert + expect(firstItem).toBeTruthy(); + }); + + it('should return false when it is not the last item of the array', () => { + // Arrange + const indexes: FieldVm[] = [ + { id: '1', name: 'Index1', isUnique: false, sparse: false, fields: [] }, + { id: '2', name: 'Index2', isUnique: false, sparse: false, fields: [] }, + ]; + //Act + const firstItem = isFirstItemInArray(indexes, '1'); + + //Assert + expect(firstItem).toBeFalsy(); + }); + + it('should return false when it is not the first item of the array', () => { + // Arrange + const indexes: FieldVm[] = [ + { id: '1', name: 'Index1', isUnique: false, sparse: false, fields: [] }, + { id: '2', name: 'Index2', isUnique: false, sparse: false, fields: [] }, + ]; + //Act + const firstItem = isFirstItemInArray(indexes, '2'); + + //Assert + expect(firstItem).toBeFalsy(); + }); +}); diff --git a/src/pods/manage-index/components/commands/commands.business-last-item.spec.ts b/src/pods/manage-index/components/commands/commands.business-last-item.spec.ts new file mode 100644 index 00000000..bb7b7cae --- /dev/null +++ b/src/pods/manage-index/components/commands/commands.business-last-item.spec.ts @@ -0,0 +1,45 @@ +import { FieldVm } from '../../manage-index.vm'; +import { isLastItemInArray } from './commands.business'; + +describe('isLastItemInArray', () => { + it('should return true when it is the last item of the array', () => { + // Arrange + const indexes: FieldVm[] = [ + { id: '1', name: 'Index1', isUnique: false, sparse: false, fields: [] }, + { id: '2', name: 'Index2', isUnique: false, sparse: false, fields: [] }, + ]; + + //Act + const firstItem = isLastItemInArray(indexes, '2'); + + //Assert + expect(firstItem).toBeTruthy(); + }); + it('should return false when it is not the last item of the array', () => { + // Arrange + const indexes: FieldVm[] = [ + { id: '1', name: 'Index1', isUnique: false, sparse: false, fields: [] }, + { id: '2', name: 'Index2', isUnique: false, sparse: false, fields: [] }, + ]; + + //Act + const firstItem = isLastItemInArray(indexes, '1'); + + //Assert + expect(firstItem).toBeFalsy(); + }); + + it('should return false when it is not the last item of the array', () => { + // Arrange + const indexes: FieldVm[] = [ + { id: '1', name: 'Index1', isUnique: false, sparse: false, fields: [] }, + { id: '2', name: 'Index2', isUnique: false, sparse: false, fields: [] }, + ]; + + //Act + const firstItem = isLastItemInArray(indexes, '1'); + + //Assert + expect(firstItem).toBeFalsy(); + }); +}); diff --git a/src/pods/manage-index/components/commands/commands.business.ts b/src/pods/manage-index/components/commands/commands.business.ts new file mode 100644 index 00000000..412c060a --- /dev/null +++ b/src/pods/manage-index/components/commands/commands.business.ts @@ -0,0 +1,8 @@ +import { GUID } from '@/core/model'; +import { FieldVm } from '../../manage-index.vm'; + +export const isLastItemInArray = (array: FieldVm[], id: GUID): boolean => + array[array.length - 1].id === id; + +export const isFirstItemInArray = (array: FieldVm[], id: GUID): boolean => + array[0].id === id; diff --git a/src/pods/manage-index/components/commands/commands.component.tsx b/src/pods/manage-index/components/commands/commands.component.tsx new file mode 100644 index 00000000..689f4166 --- /dev/null +++ b/src/pods/manage-index/components/commands/commands.component.tsx @@ -0,0 +1,46 @@ +import { Add } from '@/common/components/icons/add-icon.component'; +import { CommandIconButton } from './command-icon-button'; +import { FieldVm } from '../../manage-index.vm'; +import { GUID, GenerateGUID } from '@/core/model'; +import { TrashIcon } from '@/common/components'; + +interface Props { + onDeleteIndex: (indexId: GUID) => void; + onAddIndex: (fieldId: GUID, isChildren: boolean, newfieldId: GUID) => void; + index: FieldVm; + indexes: FieldVm[]; + onMoveDown: (indexId: GUID) => void; + onMoveUp: (indexId: GUID) => void; + isDeleteVisible: boolean; + labelAddIndex?: string; +} + +const REMOVE_ICON = 'Remove'; +const ADD = 'Add Index'; + +export const Commands: React.FC = (props: Props) => { + const { + index: index, + onDeleteIndex: onDeleteIndex, + onAddIndex: onAddIndex, + isDeleteVisible, + labelAddIndex: labelAddIndex, + } = props; + + return ( + <> + } + onClick={() => onAddIndex(index.id, false, GenerateGUID())} + ariaLabel={labelAddIndex || ADD} + /> + {isDeleteVisible && ( + } + onClick={() => onDeleteIndex(index.id)} + ariaLabel={REMOVE_ICON + index.name} + /> + )} + + ); +}; diff --git a/src/pods/manage-index/components/index.tsx b/src/pods/manage-index/components/index.tsx new file mode 100644 index 00000000..4800f3b5 --- /dev/null +++ b/src/pods/manage-index/components/index.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import classes from '../manage-index.module.css'; +import { Reorder, motion, useDragControls } from 'framer-motion'; +import { Commands } from './commands/commands.component'; +import { DragDropIcon, Checkbox } from '@/common/components'; +import { FieldVm } from '../manage-index.vm'; +import { GUID } from '@/core/model'; + +interface Props { + index: FieldVm; + indexes: FieldVm[]; + level: number; + expanded: Set; + toggleExpand: (id: GUID) => void; + expand: (id: GUID) => void; + updateValue: ( + index: FieldVm, + id: K, + value: FieldVm[K] + ) => void; + nameInputRefRecord: React.RefObject>; + onDelete: (id: GUID) => void; + onAdd: (id: GUID) => void; + onMoveDown: (id: GUID) => void; + onMoveUp: (id: GUID) => void; + isDeleteVisible: boolean; + labelAddIndex?: string; +} + +const INPUT_NAME = 'Index '; +const CHECKBOX_ISUNIQUE = 'Checkbox isUnique for index '; +export const Index: React.FC = props => { + const { + index, + indexes, + level, + onAdd, + onDelete, + onMoveDown, + onMoveUp, + updateValue, + nameInputRefRecord, + isDeleteVisible, + labelAddIndex, + } = props; + const variantsItem = { + left: { + opacity: 0, + x: -200, + scale: 0.8, + }, + stay: { + opacity: 1, + x: 0, + scale: 1, + transition: { duration: 0.3 }, + }, + }; + const dragControls = useDragControls(); + const handleAdd = (id: GUID) => { + onAdd(id); + }; + + const handlerPointerDown = (e: React.PointerEvent) => { + dragControls.start(e); + e.currentTarget.style.cursor = 'grabbing'; + }; + + const assignRef = (el: HTMLInputElement | null, id: string) => { + if (el && nameInputRefRecord?.current) { + nameInputRefRecord.current[id] = el; + } + }; + + return ( + + +
+ handlerPointerDown(e)} + onPointerUp={e => (e.currentTarget.style.cursor = 'grab')} + aria-hidden="true" + tabIndex={-1} + > + + +
+
+
+ { + updateValue(index, 'name', e.target.value); + }} + ref={el => assignRef(el, index.id)} + aria-label={INPUT_NAME + index.name} + /> +
+
+
+
+ { + updateValue(index, 'fieldsString', e.target.value); + }} + ref={el => assignRef(el, index.id)} + aria-label={INPUT_NAME + index.name} + /> +
+
+ updateValue(index, 'isUnique', !index.isUnique)} + ariaLabel={CHECKBOX_ISUNIQUE + index.name} + > +
+
+ updateValue(index, 'sparse', !index.sparse)} + ariaLabel={CHECKBOX_ISUNIQUE + index.name} + > +
+
+ { + updateValue(index, 'partialFilterExpression', e.target.value); + }} + ref={el => assignRef(el, index.id)} + aria-label={INPUT_NAME + index.name} + /> +
+
+ +
+
+ + + ); +}; diff --git a/src/pods/manage-index/components/nested-manage-index-grid.tsx b/src/pods/manage-index/components/nested-manage-index-grid.tsx new file mode 100644 index 00000000..f134a159 --- /dev/null +++ b/src/pods/manage-index/components/nested-manage-index-grid.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import classes from '../manage-index.module.css'; +import { GUID } from '@/core/model'; +import { FieldVm } from '../manage-index.vm'; + +import { AnimatePresence, Reorder } from 'framer-motion'; +import { Index } from './index'; + +interface NestedManageIndexGridProps { + id?: GUID; + indexes: FieldVm[]; + level: number; + expanded: Set; + toggleExpand: (fieldId: string) => void; + expand: (fieldId: string) => void; + updateValue: ( + index: FieldVm, + id: K, + value: FieldVm[K] + ) => void; + onDelete: (id: GUID) => void; + onAdd: (id: GUID) => void; + nameInputRefRecord: React.RefObject>; + onMoveDown: (id: GUID) => void; + onMoveUp: (id: GUID) => void; + onDrag?: (index: FieldVm[], id?: GUID) => void; + isDeleteVisible: boolean; + labelAddIndex?: string; +} + +export const NestedManageIndexGrid: React.FC = ({ + id, + indexes, + level, + expanded, + toggleExpand, + expand, + updateValue, + onDelete, + onAdd, + nameInputRefRecord, + onMoveDown, + onMoveUp, + onDrag, + isDeleteVisible, + labelAddIndex, +}) => { + const variantsGroup = { + open: { opacity: 1, height: 'auto' }, + collapsed: { opacity: 0, height: 0 }, + }; + return ( + (onDrag ? onDrag(indexes, id) : null)} + className={classes.nestedGrid} + key={level} + initial="collapsed" + animate="open" + exit="collapsed" + variants={variantsGroup} + transition={{ duration: 0.8 }} + > + + {indexes.map(item => ( + + ))} + + + ); +}; diff --git a/src/pods/manage-index/index.ts b/src/pods/manage-index/index.ts new file mode 100644 index 00000000..09ec1843 --- /dev/null +++ b/src/pods/manage-index/index.ts @@ -0,0 +1 @@ +export * from './manage-index.pod'; diff --git a/src/pods/manage-index/manage-index.business.ts b/src/pods/manage-index/manage-index.business.ts new file mode 100644 index 00000000..d70718ba --- /dev/null +++ b/src/pods/manage-index/manage-index.business.ts @@ -0,0 +1,261 @@ +import { produce } from 'immer'; +import { FieldVm } from './manage-index.vm'; +import { GUID } from '@/core/model'; +import * as editIndexVm from './manage-index.vm'; +import * as canvasVm from '@/core/providers/canvas-schema'; +import { + clonify, + isEqual, + isNullOrWhiteSpace, + parseManageIndexFields, +} from '@/core/functions'; +import { errorHandling, Output } from '@/core/model/errorHandling'; + +export interface UpdateIndexValueParams { + indexToUpdate: editIndexVm.FieldVm; + key: K; + value: editIndexVm.FieldVm[K]; +} + +export const updateIndexValueLogic = ( + table: canvasVm.TableVm, + params: UpdateIndexValueParams +) => { + return produce(table, draftTable => { + const findAndUpdateIndex = (currentTable: canvasVm.TableVm): boolean => { + const formerIndex = currentTable.indexes?.find( + f => f.id === params.indexToUpdate.id + ); + if (formerIndex) { + formerIndex[params.key] = params.value; + return true; + } + if (!currentTable.indexes) currentTable.indexes = []; + params.indexToUpdate[params.key] = params.value; + currentTable.indexes.push(params.indexToUpdate); + return true; + }; + + findAndUpdateIndex(draftTable); + }); +}; + +export const removeIndex = ( + table: canvasVm.TableVm, + indexId: GUID +): canvasVm.TableVm => { + return produce(table, draftTable => { + const remove = (indexes: FieldVm[]): void => { + for (let i = 0; i < indexes.length; i++) { + if (indexes[i].id === indexId) { + indexes.splice(i, 1); + return; + } + } + }; + if (table.indexes) remove(draftTable.indexes as editIndexVm.FieldVm[]); + }); +}; + +export const addIndexLogic = ( + currentTable: canvasVm.TableVm, + indexId: GUID, + newFieldId: GUID +) => { + return produce(currentTable, draftTable => { + const findAndAddIndex = (indexes: editIndexVm.FieldVm[]): boolean => { + const foundIndex = indexes.findIndex(f => f.id === indexId); + if (foundIndex != -1) { + indexes.splice( + foundIndex + 1, + 0, + editIndexVm.createDefaultIndex(currentTable.tableName, newFieldId) + ); + return true; + } + return false; + }; + if (currentTable.indexes) + findAndAddIndex(draftTable.indexes as editIndexVm.FieldVm[]); + }); +}; + +export const moveDown = ( + table: canvasVm.TableVm, + indexId: GUID +): canvasVm.TableVm => { + return produce(table, draftTable => { + const _moveDown = (indexes: FieldVm[]): void => { + for (let i = 0; i < indexes.length; i++) { + const field = indexes[i]; + if (field.id === indexId && i < indexes.length - 1) { + const temp = indexes[i]; + indexes[i] = indexes[i + 1]; + indexes[i + 1] = temp; + return; + } + } + }; + + _moveDown(draftTable.indexes as editIndexVm.FieldVm[]); + }); +}; + +export const moveUp = ( + table: canvasVm.TableVm, + indexId: GUID +): canvasVm.TableVm => { + return produce(table, draftTable => { + const _moveUp = (indexes: FieldVm[]): void => { + for (let i = 0; i < indexes.length; i++) { + const field = indexes[i]; + if (field.id === indexId && i > 0) { + const temp = indexes[i]; + indexes[i] = indexes[i - 1]; + indexes[i - 1] = temp; + return; + } + } + }; + + _moveUp(draftTable.indexes as editIndexVm.FieldVm[]); + }); +}; + +export const findIndex = ( + indexes: editIndexVm.FieldVm[], + id: GUID +): editIndexVm.FieldVm | undefined => { + for (const item of indexes) { + if (item.id === id) return item; + } + return undefined; +}; + +//of course this needs unit test +export const apply = (table: canvasVm.TableVm): Output => { + const result: Output = { + errorHandling: { + isSuccessful: true, + }, + }; + + const _table = clonify(table); + const errorFound = _table.indexes?.find( + x => isNullOrWhiteSpace(x.name) || isNullOrWhiteSpace(x.fieldsString) + ); + if (!_table.indexes || errorFound) { + const error = 'Please make sure that you provided the correct info'; + result.errorHandling.errorMessage = error; + result.errorHandling.isSuccessful = false; + return result; + } + + if (_table.indexes?.length > 0) { + for (let i = 0; i < _table.indexes.length; i++) { + const item = _table.indexes[i]; + const fields = parseManageIndexFields(item.fieldsString); + _table.indexes[i].fields = []; + _table.indexes[i].fields.push(...fields); + } + + let error: string = ''; + _table.indexes.some(elem => { + elem.fields.some(fld => { + const found = doesColumnExist(table, fld.name); + if (!found) { + error = `Field name provided(${fld.name}) does not exist in the table schema.`; + return true; + } + + if ( + !isNullOrWhiteSpace(fld.orderMethod) && + !isEqual(fld.orderMethod, 'Ascending') && + !isEqual(fld.orderMethod, 'Descending') + ) { + error = `The order method provided(${fld.orderMethod}) is incorrect.`; + return true; + } + }); + if (!isNullOrWhiteSpace(error)) return true; + }); + + if (!isNullOrWhiteSpace(error)) { + result.errorHandling.isSuccessful = false; + result.errorHandling.errorMessage = error; + return result; + } + } + + result.data = _table; + + return result; +}; + +const doesColumnExist = (table: canvasVm.TableVm, searchInput: string) => { + const keys = searchInput.split('.'); + const rootKey = keys[0]; + + for (const field of table.fields) { + if (field.name === rootKey) { + let current = field; + for (let i = 1; i < keys.length; i++) { + const key = keys[i]; + if (current.children && Array.isArray(current.children)) { + const child = current.children.find(child => child.name === key); + if (child) { + current = child; // Move deeper into the hierarchy + } else { + return false; // Column not found at this level + } + } else { + return false; // No children to traverse further + } + } + return true; // Successfully traversed all keys + } + } + + return false; +}; + +export const indexDuplicateNameChecking = ( + table: canvasVm.TableVm, + schema: canvasVm.DatabaseSchemaVm +): errorHandling => { + const result: errorHandling = { + isSuccessful: true, + }; + + // Check the duplicates index name in the current table + table.indexes?.some(idx => { + const dupFound = table.indexes?.find( + x => isEqual(x.name, idx.name) && x.id !== idx.id + ); + + if (dupFound) { + result.errorMessage = `Duplicate index name found in table (${table.tableName}) with index name (${dupFound.name})`; + result.isSuccessful = false; + return true; + } + }); + + if (!result.isSuccessful) return result; + + // Check the duplicates index name in the whole schema + schema.tables.some(tbl => { + if (isEqual(tbl.tableName, table.tableName)) return false; + + table.indexes?.some(idx => { + const dupFound = tbl.indexes?.find(x => isEqual(x.name, idx.name)); + + if (dupFound) { + result.errorMessage = `Duplicate index name found in table (${tbl.tableName}) with index name (${dupFound.name})`; + result.isSuccessful = false; + return true; + } + }); + }); + + return result; +}; diff --git a/src/pods/manage-index/manage-index.component.tsx b/src/pods/manage-index/manage-index.component.tsx new file mode 100644 index 00000000..116ba7c3 --- /dev/null +++ b/src/pods/manage-index/manage-index.component.tsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react'; +import classes from './manage-index.module.css'; +import { createDefaultIndex, FieldVm } from './manage-index.vm'; +import { GUID } from '@/core/model'; +import { useInputRefFocus } from './use-input-focus.hook'; +import { TableVm } from '@/core/providers'; +import { NestedManageIndexGrid } from './components/nested-manage-index-grid'; + +interface Props { + table: TableVm; + updateValue: ( + field: FieldVm, + id: K, + value: FieldVm[K] + ) => void; + onDelete: (id: GUID) => void; + onAdd: (id: GUID, newIndexId: GUID) => void; + onMoveDown: (id: GUID) => void; + onMoveUp: (id: GUID) => void; + onDrag?: (index: FieldVm[], id?: GUID) => void; +} + +export const ManageIndexComponent: React.FC = props => { + const { table, updateValue, onDelete, onAdd, onMoveDown, onMoveUp, onDrag } = + props; + + const { handleAdd, nameInputRefRecord } = useInputRefFocus(onAdd); + + const [expanded, setExpanded] = useState>(new Set()); + + const expand = (indexId: GUID) => { + setExpanded(prev => { + const newExpanded = new Set(prev); + if (!newExpanded.has(indexId)) { + newExpanded.add(indexId); + } + return newExpanded; + }); + }; + + const toggleExpand = (indexId: GUID) => { + setExpanded(prev => { + const newExpanded = new Set(prev); + if (newExpanded.has(indexId)) { + newExpanded.delete(indexId); + } else { + newExpanded.add(indexId); + } + return newExpanded; + }); + }; + + return ( + <> +
+ +
+ +
+ Instructions: +
  • + Please note that the Fields column should be provided as the + following. FieldName1 (Ascending/Descending: optional), FieldName2 + (Ascending/Descending: optional). The comma separator is needed if + more than one field is needed +
  • +
  • + Partial Filter Expression: has to be provided without the initial{' '} + {'{}'}. Example= {`email: { $exists: true, $ne: null }`} +
  • +
    + +
    +
    +
    Name
    +
    Fields
    +
    Unique
    +
    Sparse
    +
    Partial Filter Expression
    +
    Actions
    +
    +
    + + +
    + + ); +}; diff --git a/src/pods/manage-index/manage-index.module.css b/src/pods/manage-index/manage-index.module.css new file mode 100644 index 00000000..0b659053 --- /dev/null +++ b/src/pods/manage-index/manage-index.module.css @@ -0,0 +1,222 @@ +.tableEditor { + display: grid; + grid-template-columns: auto 60px 60px 150px 80px 60px 250px; + margin: var(--space-md); + margin-top: 0; + min-width: 800px; +} +.tableEditor ul { + padding: 0; + margin: 0; +} + +.fieldRow, +.header-row { + display: grid; + grid-template-columns: auto 60px 60px 150px 80px 60px 250px; + grid-column: 1 / -1; + border: 1px solid var(--secondary-border-color); + border-top-width: 0.5px; + border-bottom-width: 0.5px; + background-color: var(--background-dialog); +} + +.nestedGrid ul li:last-child { + border-bottom: 0.5px solid var(--secondary-border-color); +} + +.tableEditor > ul > li:last-child { + border-bottom: 1px solid var(--secondary-border-color); + border-bottom-left-radius: var(--border-radius-table); + border-bottom-right-radius: var(--border-radius-table); +} + +.tableEditor > ul > li:first-child { + border-top: none; +} + +.nestedGrid { + display: grid; + grid-template-columns: auto 60px 60px 150px 80px 60px 250px; + grid-column: 1 / -1; +} +.content { + display: grid; + grid-column: 1 / -1; + grid-template-columns: auto 60px 60px 150px 80px 60px 250px; + transform-origin: top center; +} +.fieldCell, +.headerCell { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-cells); +} + +.fieldCell + .fieldCell, +.headerCell + .headerCell { + border-left: 1px solid var(--secondary-border-color); +} + +.headerCell { + padding: var(--space-xs) 5px; + background-color: var(--edit-table-header); + color: var(--text-white); + border-top: 1px solid var(--secondary-border-color); + border-bottom: 1px solid var(--secondary-border-color); +} + +.commands-container { + display: flex; + flex-direction: row; + justify-content: flex-end; + gap: 10px; +} + +.expandCell { + display: flex; + width: 100%; + gap: 5px; +} + +@supports (grid-template-columns: subgrid) { + .tableEditor { + grid-template-columns: repeat(7, auto); + } + .fieldRow, + .header-row, + .nestedGrid { + grid-template-columns: subgrid; + } + .expandCell { + min-width: 300px; + } + + .content { + grid-template-columns: subgrid; + } +} + +/* TODO: this solution is not ideal, we would need to add more indent levels */ +/* #59 */ +/* https://github.com/Lemoncode/mongo-modeler/issues/59 */ +.indent0 { + padding-left: 5px; +} +.indent1 { + padding-left: var(--padding-table); +} +.indent2 { + padding-left: calc(var(--padding-table) * 2); +} +.indent3 { + padding-left: calc(var(--padding-table) * 3); +} +.indent4 { + padding-left: calc(var(--padding-table) * 4); +} + +.indent5 { + padding-left: calc(var(--padding-table) * 5); +} +.indent6 { + padding-left: calc(var(--padding-table) * 6); +} +.indent7 { + padding-left: calc(var(--padding-table) * 7); +} +.indent8 { + padding-left: calc(var(--padding-table) * 8); +} +.indent9 { + padding-left: calc(var(--padding-table) * 9); +} +.indent10 { + padding-left: calc(var(--padding-table) * 10); +} + +/*The top position of the two position sticky's are +related to each other and to the dialog header*/ +.header-row { + position: sticky; + top: 7.1rem; + z-index: 1; + border: none; + border-radius: 0px; + padding-top: var(--space-md); +} + +.header-row .headerCell:first-child { + border-top-left-radius: var(--border-radius-table); + border-left: 1px solid var(--secondary-border-color); +} + +.header-row .headerCell:last-child { + border-top-right-radius: var(--border-radius-table); + border-right: 1px solid var(--secondary-border-color); +} + +.table-name { + background-color: var(--background-dialog); + position: sticky; + top: 3rem; + left: 0px; + padding-top: var(--space-md); + padding-bottom: var(--space-md); + z-index: 2; + text-align: left; + border-bottom: 1px solid var(--primary-border-color); +} + +.table-name label { + display: flex; + align-items: baseline; + gap: 10px; +} + +.table-name input { + max-width: 190px; +} + +.fieldCell button { + display: flex; + align-items: center; + justify-content: center; + padding: 0.4em 0.6em; + font-size: var(--fs-m); + min-width: 38px; + min-height: 31px; +} + +.input-name { + width: 100%; + height: 100%; + display: flex; + align-items: center; +} + +.button-space { + padding: 0.4em 0.6em; + font-size: var(--fs-m); + min-width: 30px; + min-height: 30px; +} + +.expandCell button { + padding: 3px; + min-width: 30px; + min-height: 30px; +} + +.fieldCell .drag-button { + cursor: grab; + padding: 3px; + min-width: 30px; + min-height: 30px; +} +.drag-button svg { + pointer-events: none; +} diff --git a/src/pods/manage-index/manage-index.pod.tsx b/src/pods/manage-index/manage-index.pod.tsx new file mode 100644 index 00000000..3b83f89e --- /dev/null +++ b/src/pods/manage-index/manage-index.pod.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import * as canvasVm from '@/core/providers/canvas-schema'; +import * as editIndexVm from './manage-index.vm'; +import { ManageIndexComponent } from './manage-index.component'; +import { GUID } from '@/core/model'; +import { + addIndexLogic, + moveDown, + moveUp, + removeIndex, + apply, + updateIndexValueLogic, +} from './manage-index.business'; + +interface Props { + table: canvasVm.TableVm; + onSave: (table: canvasVm.TableVm) => void; + onClose: () => void; +} + +export const ManageIndexPod: React.FC = props => { + const { table, onSave, onClose } = props; + + const [editTableIndex, setEditTableIndex] = + React.useState(table); + + const handleSubmit = (table: canvasVm.TableVm) => { + const saveResult = apply(table); + if (!saveResult.errorHandling.isSuccessful) { + alert(saveResult.errorHandling.errorMessage); + return; + } + onSave(saveResult.data ?? table); + }; + + const updateValue = ( + indexToUpdate: editIndexVm.FieldVm, + key: K, + value: editIndexVm.FieldVm[K] + ) => { + setEditTableIndex(currentTable => + updateIndexValueLogic(currentTable, { + indexToUpdate: indexToUpdate as editIndexVm.FieldVm, + key, + value, + }) + ); + }; + + const onDelete = (indexId: GUID) => { + setEditTableIndex(currentTable => removeIndex(currentTable, indexId)); + }; + + const onAdd = (indexId: GUID, newIndexId: GUID) => { + setEditTableIndex(currentTable => + addIndexLogic(currentTable, indexId, newIndexId) + ); + }; + + const onMoveDown = (id: GUID) => { + setEditTableIndex(currentTable => moveDown(currentTable, id)); + }; + + const onMoveUp = (id: GUID) => { + setEditTableIndex(currentTable => moveUp(currentTable, id)); + }; + + return ( + <> + +
    + + +
    + + ); +}; diff --git a/src/pods/manage-index/manage-index.vm.ts b/src/pods/manage-index/manage-index.vm.ts new file mode 100644 index 00000000..054cbf28 --- /dev/null +++ b/src/pods/manage-index/manage-index.vm.ts @@ -0,0 +1,24 @@ +import { GUID, GenerateGUID } from '@/core/model'; +import { IndexField } from '@/core/providers'; + +export interface FieldVm { + id: GUID; + name: string; + isUnique: boolean; + sparse: boolean; + fields: IndexField[]; + fieldsString?: string; + partialFilterExpression?: string; +} + +export const createDefaultIndex = ( + tableName: string, + guid?: GUID +): FieldVm => ({ + id: guid ? guid : GenerateGUID(), + name: `${tableName}_idx_newIndex`, + isUnique: false, + sparse: false, + fields: [], + fieldsString: '', +}); diff --git a/src/pods/manage-index/use-input-focus.hook.ts b/src/pods/manage-index/use-input-focus.hook.ts new file mode 100644 index 00000000..968febce --- /dev/null +++ b/src/pods/manage-index/use-input-focus.hook.ts @@ -0,0 +1,28 @@ +import { GUID, GenerateGUID } from '@/core/model'; +import React from 'react'; + +export const useInputRefFocus = ( + onAdd: (id: GUID, newIndexId: GUID) => void +) => { + const nameInputRefRecord = React.useRef>({}); + + const [newIndexId, setNewIndexId] = React.useState(''); + + const handleAdd = (id: GUID) => { + const newIndexId = GenerateGUID(); + setNewIndexId(newIndexId); + onAdd(id, newIndexId); + }; + + React.useEffect(() => { + const input = nameInputRefRecord.current[newIndexId]; + + if (input) { + input.focus(); + input.select(); + setNewIndexId(''); + } + }, [newIndexId, nameInputRefRecord.current]); + + return { handleAdd, nameInputRefRecord }; +}; diff --git a/src/pods/toolbar/components/export-button/export-button.business.ts b/src/pods/toolbar/components/export-button/export-button.business.ts index 4d45e168..e46ac760 100644 --- a/src/pods/toolbar/components/export-button/export-button.business.ts +++ b/src/pods/toolbar/components/export-button/export-button.business.ts @@ -1,5 +1,6 @@ import { FieldVm, TableVm, TABLE_CONST } from '@/core/providers'; import { doTablesOverlap } from './export-coordinate.helpers'; +import { isNullOrWhiteSpace } from '@/core/functions'; export const getMaxPositionXFromTables = (tables: TableVm[]): number => tables.length === 0 ? 0 : Math.max(...tables.map(table => table.x)); @@ -77,10 +78,10 @@ export const placeTableWithoutOverlap = ( }; export const placeAllTablesWithoutOverlap = (tables: TableVm[]): TableVm[] => { - let placedTables: TableVm[] = []; + const placedTables: TableVm[] = []; - for (let table of tables) { - let newTable = placeTableWithoutOverlap(table, placedTables); + for (const table of tables) { + const newTable = placeTableWithoutOverlap(table, placedTables); placedTables.push(newTable); } @@ -116,7 +117,10 @@ export const getPropertyJsonSchema = (field: FieldVm): string => { return `"${field.name}": { bsonType: "${field.type}" }`; }; -export const getPropertiesJsonSchema = (fields: FieldVm[], useTab = true): string => { +export const getPropertiesJsonSchema = ( + fields: FieldVm[], + useTab = true +): string => { const separator = useTab ? ',\n ' : ', '; return fields.map(getPropertyJsonSchema).join(separator); }; @@ -141,11 +145,47 @@ export const getSchemaScriptFromTableVm = (table: TableVm): string => { }, }, }, -});`; + });`; - return schemaScript; + const createIndexes: string = generateIndexScript(table); + + return `${schemaScript}\n${createIndexes}`; }; +const generateIndexScript = (table: TableVm): string => { + let createIndexes: string = ''; + if (table.indexes && table.indexes?.length > 0) { + const t = table.indexes.map(idx => { + const fields = idx.fields + .map(item => { + let oM: string = ''; + switch (item.orderMethod.toLowerCase()) { + case 'ascending': + oM = ':1'; + break; + case 'descending': + oM = ':-1'; + break; + default: + oM = ':1'; + } + return `"${item.name}" ${oM}`; + }) + .join(', '); + + return `db.${table.tableName}.createIndex( + { ${fields} }, + { + name: "${idx.name}", + unique:${idx.isUnique ? 'true' : 'false'}, + sparse:${idx.sparse ? 'true' : 'false'}, + ${isNullOrWhiteSpace(idx.partialFilterExpression) ? '' : `partialFilterExpression:{${idx.partialFilterExpression}}`} + })`; + }); + createIndexes = t.join(';\n'); + } + return createIndexes; +}; export const getSchemaScriptFromTableVmArray = (tables: TableVm[]): string => { return tables.map(getSchemaScriptFromTableVm).join('\n\n'); }; diff --git a/src/pods/toolbar/components/index.ts b/src/pods/toolbar/components/index.ts index 98db55bc..fa14ae7c 100644 --- a/src/pods/toolbar/components/index.ts +++ b/src/pods/toolbar/components/index.ts @@ -2,6 +2,7 @@ export * from './toolbar-button'; export * from './theme-toggle-button'; export * from './zoom-button'; export * from './relation-button'; +export * from './manage-index-button'; export * from './canvas-setting-button'; export * from './export-button'; export * from './new-button'; diff --git a/src/pods/toolbar/components/manage-index-button/index-button.component.tsx b/src/pods/toolbar/components/manage-index-button/index-button.component.tsx new file mode 100644 index 00000000..15b4172f --- /dev/null +++ b/src/pods/toolbar/components/manage-index-button/index-button.component.tsx @@ -0,0 +1,41 @@ +import { useModalDialogContext } from '@/core/providers/modal-dialog-provider'; +import { ToolbarButton } from '@/pods/toolbar/components/toolbar-button'; +import classes from '@/pods/toolbar/toolbar.pod.module.css'; +import { + IndexVm, + useCanvasSchemaContext, +} from '@/core/providers/canvas-schema'; +import { MANAGE_INDEX_TITLE } from './../../../../common/components/modal-dialog/modal-dialog.const'; + +export const ManageIndexButton = () => { + const { openModal, closeModal } = useModalDialogContext(); + const { canvasSchema, addIndexes } = useCanvasSchemaContext(); + + const handleChangeCanvasSchema = (indexes: IndexVm[]) => { + addIndexes('tableId', indexes); + closeModal(); + }; + + const handleClick = () => { + openModal('Test1', MANAGE_INDEX_TITLE); + handleChangeCanvasSchema([ + { + id: '', + name: 'test', + isUnique: false, + sparse: false, + fields: [], + }, + ]); + }; + + return ( + + ); +}; diff --git a/src/pods/toolbar/components/manage-index-button/index.ts b/src/pods/toolbar/components/manage-index-button/index.ts new file mode 100644 index 00000000..ed73fe5a --- /dev/null +++ b/src/pods/toolbar/components/manage-index-button/index.ts @@ -0,0 +1 @@ +export * from './index-button.component';