diff --git a/src/common/components/icons/index.ts b/src/common/components/icons/index.ts index 56c4ea26..d2207e5a 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 45b0c6fd..fd12f66d 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 29767cba..46d92d10 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 }) @@ -236,8 +257,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 4a2963c0..e3a14887 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, @@ -122,6 +126,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 = () => { @@ -252,7 +278,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/components/database-table-row.component.tsx b/src/pods/canvas/components/table/components/database-table-row.component.tsx index a81bd891..026becd4 100644 --- a/src/pods/canvas/components/table/components/database-table-row.component.tsx +++ b/src/pods/canvas/components/table/components/database-table-row.component.tsx @@ -4,6 +4,7 @@ import { TABLE_CONST } from '@/core/providers/canvas-schema/canvas.const'; import { TruncatedText } from './truncated-text.component'; import classes from '../database-table.module.css'; import { calculateColumNameWidth } from '../database-table.business'; +import { isNullOrWhiteSpace } from '@/core/functions'; interface Props { tableInfo: TableVm; @@ -41,6 +42,25 @@ export const DatabaseTableRow: React.FC = props => { field.type === 'object' && (field.children?.length ?? 0) > 0; const isExpanded = !field.isCollapsed; + const indexFlag = (fieldName: string): string => { + const found = tableInfo.indexes?.find(x => + x.fields?.find( + z => z.name == fieldName || z.name.endsWith(`.${fieldName}`) + ) + ); + + let result: string = ''; + + if (found) { + result = 'i'; + if (found.isUnique) result = 'u' + result; + } + + return result; + }; + + const indexFlagStr = indexFlag(field.name); + return ( @@ -55,7 +75,10 @@ export const DatabaseTableRow: React.FC = props => { )} = props => { level * TABLE_CONST.LEVEL_INDENTATION } height={TABLE_CONST.FONT_SIZE} + style={ + indexFlagStr == 'i' + ? { fontWeight: 'bold' } + : indexFlagStr == 'ui' + ? { + fontWeight: 'bold', + fontStyle: 'italic', + } + : {} + } /> = props => { @@ -26,6 +27,7 @@ export const TruncatedText: React.FC = props => { y={y + height} clipPath={`url(#clip_${id})`} className={!textClass ? classes.tableTextRow : textClass} + style={props.style} > {text} 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.component.tsx b/src/pods/edit-table/edit-table.component.tsx index fcb9259e..02c060f2 100644 --- a/src/pods/edit-table/edit-table.component.tsx +++ b/src/pods/edit-table/edit-table.component.tsx @@ -15,6 +15,7 @@ interface Props { onDeleteField: (fieldId: GUID) => void; onAddField: (fieldId: GUID, isChildren: boolean, newFieldId: GUID) => void; updateTableName: (value: string) => void; + updateTableComment: (value: string) => void; onMoveDownField: (fieldId: GUID) => void; onMoveUpField: (fieldId: GUID) => void; onDragField: (fields: FieldVm[], id?: GUID) => void; @@ -27,6 +28,7 @@ export const EditTableComponent: React.FC = props => { onDeleteField, onAddField, updateTableName, + updateTableComment, onMoveDownField, onMoveUpField, onDragField, @@ -62,6 +64,10 @@ export const EditTableComponent: React.FC = props => { updateTableName(e.currentTarget.value); }; + const handleChangeTableComment = (e: React.ChangeEvent) => { + updateTableComment(e.currentTarget.value); + }; + return ( <>
@@ -75,6 +81,17 @@ export const EditTableComponent: React.FC = props => { />
+
+