diff --git a/examples/mapping-example/Sources/ExampleCRM/entities/Address.entity.cm b/examples/mapping-example/Sources/ExampleCRM/entities/Address.entity.cm index e9bff3c3..6e3d6ddb 100644 --- a/examples/mapping-example/Sources/ExampleCRM/entities/Address.entity.cm +++ b/examples/mapping-example/Sources/ExampleCRM/entities/Address.entity.cm @@ -6,6 +6,7 @@ entity: - id: CustomerID name: "CustomerID" datatype: "Integer" + identifier: true - id: Street name: "Street" datatype: "Text" diff --git a/examples/mapping-example/Sources/ExampleCRM/entities/Customer.entity.cm b/examples/mapping-example/Sources/ExampleCRM/entities/Customer.entity.cm index 78683740..d3ef8026 100644 --- a/examples/mapping-example/Sources/ExampleCRM/entities/Customer.entity.cm +++ b/examples/mapping-example/Sources/ExampleCRM/entities/Customer.entity.cm @@ -2,24 +2,25 @@ entity: id: Customer name: "Customer" attributes: - - id: Id - name: "Id" - datatype: "Integer" - - id: FirstName - name: "FirstName" - datatype: "Text" - - id: LastName - name: "LastName" - datatype: "Text" - - id: City - name: "City" - datatype: "Text" - - id: Country - name: "Country" - datatype: "Text" - - id: Phone - name: "Phone" - datatype: "Text" - - id: BirthDate - name: "BirthDate" - datatype: "DateTime" \ No newline at end of file + - id: Id + name: "Id" + datatype: "Integer" + identifier: true + - id: FirstName + name: "FirstName" + datatype: "Text" + - id: LastName + name: "LastName" + datatype: "Text" + - id: City + name: "City" + datatype: "Text" + - id: Country + name: "Country" + datatype: "Text" + - id: Phone + name: "Phone" + datatype: "Text" + - id: BirthDate + name: "BirthDate" + datatype: "DateTime" \ No newline at end of file diff --git a/examples/mapping-example/Sources/ExampleCRM/entities/Order.entity.cm b/examples/mapping-example/Sources/ExampleCRM/entities/Order.entity.cm index 0d804945..62184e8a 100644 --- a/examples/mapping-example/Sources/ExampleCRM/entities/Order.entity.cm +++ b/examples/mapping-example/Sources/ExampleCRM/entities/Order.entity.cm @@ -6,6 +6,7 @@ entity: - id: Id name: "Id" datatype: "Integer" + identifier: true - id: OrderDate name: "OrderDate" datatype: "Integer" diff --git a/examples/mapping-example/Sources/ExampleMasterdata/entities/Country.entity.cm b/examples/mapping-example/Sources/ExampleMasterdata/entities/Country.entity.cm index c760ff72..183df446 100644 --- a/examples/mapping-example/Sources/ExampleMasterdata/entities/Country.entity.cm +++ b/examples/mapping-example/Sources/ExampleMasterdata/entities/Country.entity.cm @@ -2,9 +2,10 @@ entity: id: Country name: "Country" attributes: - - id: Code - name: "Code" - datatype: "Varchar" - - id: Name - name: "name" - datatype: "Varchar" \ No newline at end of file + - id: Code + name: "Code" + datatype: "Varchar" + identifier: true + - id: Name + name: "Name" + datatype: "Varchar" \ No newline at end of file diff --git a/extensions/crossmodel-lang/src/glsp-server/common/nodes.ts b/extensions/crossmodel-lang/src/glsp-server/common/nodes.ts index 131c4272..b66181c8 100644 --- a/extensions/crossmodel-lang/src/glsp-server/common/nodes.ts +++ b/extensions/crossmodel-lang/src/glsp-server/common/nodes.ts @@ -66,6 +66,8 @@ export class AttributeCompartmentBuilder extends GCompartmentBuilder { // Add the children of the node const attributes = getAttributes(node); - this.add(createAttributesCompartment(attributes, this.proxy.id, index)); + const attributesCompartment = new AttributesCompartmentBuilder().set(this.proxy.id); + for (const attribute of attributes) { + const attributeNode = AttributeCompartment.builder().set(attribute, index); + // increase padding left and right so we have space for the identifier icon + attributeNode.addArg('identifier', attribute.identifier).addLayoutOption('paddingLeft', 8).addLayoutOption('paddingRight', 8); + attributesCompartment.add(attributeNode.build()); + } + this.add(attributesCompartment.build()); // The DiagramNode in the langium file holds the coordinates of node this.layout('vbox') diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts index 14f737fd..1aaf7036 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts @@ -24,6 +24,7 @@ const PROPERTY_ORDER = [ 'id', 'name', 'datatype', + 'identifier', 'description', 'entity', 'parent', @@ -117,6 +118,9 @@ export class CrossModelSerializer implements Serializer { if (Array.isArray(objValue) && objValue.length === 0) { return; } + if (objKey === 'identifier' && objValue === false) { + return; + } const propKey = this.serializeKey(objKey); const propValue = this.serializeValue(objValue, indentationLevel, propKey); diff --git a/extensions/crossmodel-lang/src/language-server/entity.langium b/extensions/crossmodel-lang/src/language-server/entity.langium index ac6a5dc7..f02f7d87 100644 --- a/extensions/crossmodel-lang/src/language-server/entity.langium +++ b/extensions/crossmodel-lang/src/language-server/entity.langium @@ -18,14 +18,17 @@ Entity: interface Attribute { id: string; name: string; - datatype: string; + datatype: string; description?: string; } -interface EntityAttribute extends Attribute {} +interface EntityAttribute extends Attribute { + identifier?: boolean; +} EntityAttribute returns EntityAttribute: 'id' ':' id=ID 'name' ':' name=STRING 'datatype' ':' datatype=STRING + (identifier?='identifier' ':' ('TRUE' | 'true'))? ('description' ':' description=STRING)?; diff --git a/extensions/crossmodel-lang/src/language-server/generated/ast.ts b/extensions/crossmodel-lang/src/language-server/generated/ast.ts index 873787b3..6939f785 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/ast.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/ast.ts @@ -327,6 +327,7 @@ export function isTargetObject(item: unknown): item is TargetObject { export interface EntityAttribute extends Attribute { readonly $type: 'EntityAttribute' | 'EntityNodeAttribute'; + identifier: boolean } export const EntityAttribute = 'EntityAttribute'; @@ -521,6 +522,14 @@ export class CrossModelAstReflection extends AbstractAstReflection { ] }; } + case 'EntityAttribute': { + return { + name: 'EntityAttribute', + mandatory: [ + { name: 'identifier', type: 'boolean' } + ] + }; + } default: { return { name: type, diff --git a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts index 55bdc7e8..4fae890b 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts @@ -305,6 +305,38 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "arguments": [] } }, + { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "identifier", + "operator": "?=", + "terminal": { + "$type": "Keyword", + "value": "identifier" + } + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "TRUE" + }, + { + "$type": "Keyword", + "value": "true" + } + ] + } + ], + "cardinality": "?" + }, { "$type": "Group", "elements": [ @@ -2220,13 +2252,23 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "Interface", + "attributes": [ + { + "$type": "TypeAttribute", + "name": "identifier", + "isOptional": true, + "type": { + "$type": "SimpleType", + "primitiveType": "boolean" + } + } + ], "name": "EntityAttribute", "superTypes": [ { "$ref": "#/interfaces@0" } - ], - "attributes": [] + ] }, { "$type": "Interface", diff --git a/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json b/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json index 0de0da59..472fac31 100644 --- a/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json +++ b/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json @@ -10,7 +10,7 @@ }, { "name": "keyword.control.cross-model", - "match": "\\b(apply|attribute|attributes|child|conditions|cross-join|datatype|dependencies|description|diagram|edges|entity|expression|from|height|id|inner-join|join|left-join|mapping|mappings|name|nodes|parent|relationship|sourceNode|sources|systemDiagram|target|targetNode|type|width|x|y)\\b" + "match": "\\b(TRUE|apply|attribute|attributes|child|conditions|cross-join|datatype|dependencies|description|diagram|edges|entity|expression|from|height|id|identifier|inner-join|join|left-join|mapping|mappings|name|nodes|parent|relationship|sourceNode|sources|systemDiagram|target|targetNode|true|type|width|x|y)\\b" }, { "name": "string.quoted.double.cross-model", diff --git a/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts b/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts index 4ffa45f6..b082dcdc 100644 --- a/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts +++ b/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts @@ -41,6 +41,7 @@ describe('CrossModelLexer', () => { crossModelRoot.entity.attributes = [ { + identifier: false, $container: crossModelRoot.entity, $type: 'EntityAttribute', id: 'Attribute1', @@ -48,6 +49,7 @@ describe('CrossModelLexer', () => { datatype: 'Datatype Attribute 1' }, { + identifier: false, $container: crossModelRoot.entity, $type: 'EntityAttribute', id: 'Attribute2', @@ -66,6 +68,7 @@ describe('CrossModelLexer', () => { }; crossModelRootWithAttributesDifPlace.entity.attributes = [ { + identifier: false, $container: crossModelRoot.entity, $type: 'EntityAttribute', id: 'Attribute1', @@ -73,6 +76,7 @@ describe('CrossModelLexer', () => { datatype: 'Datatype Attribute 1' }, { + identifier: false, $container: crossModelRoot.entity, $type: 'EntityAttribute', id: 'Attribute2', diff --git a/packages/glsp-client/src/browser/model.ts b/packages/glsp-client/src/browser/model.ts index 0816113b..e2b222b1 100644 --- a/packages/glsp-client/src/browser/model.ts +++ b/packages/glsp-client/src/browser/model.ts @@ -3,11 +3,12 @@ ********************************************************************************/ import { ATTRIBUTE_COMPARTMENT_TYPE } from '@crossbreeze/protocol'; -import { GCompartment, GModelElement, Hoverable, Selectable, isSelectable } from '@eclipse-glsp/client'; +import { Args, ArgsAware, GCompartment, GModelElement, Hoverable, Selectable, isSelectable } from '@eclipse-glsp/client'; -export class AttributeCompartment extends GCompartment implements Selectable, Hoverable { +export class AttributeCompartment extends GCompartment implements Selectable, Hoverable, ArgsAware { hoverFeedback: boolean; selected: boolean; + args?: Args; static is(element?: GModelElement): element is AttributeCompartment { return !!element && isSelectable(element) && element.type === ATTRIBUTE_COMPARTMENT_TYPE; diff --git a/packages/glsp-client/src/browser/views.tsx b/packages/glsp-client/src/browser/views.tsx index e568a331..11cab6b6 100644 --- a/packages/glsp-client/src/browser/views.tsx +++ b/packages/glsp-client/src/browser/views.tsx @@ -2,6 +2,7 @@ * Copyright (c) 2024 CrossBreeze. ********************************************************************************/ /* eslint-disable react/no-unknown-property */ +/* eslint-disable max-len */ import { GCompartmentView, RenderingContext, RoundedCornerNodeView, RoundedCornerWrapper, svg } from '@eclipse-glsp/client'; import { ReactNode } from '@theia/core/shared/react'; @@ -27,7 +28,7 @@ export class AttributeCompartmentView extends GCompartmentView { override render(compartment: Readonly, context: RenderingContext): VNode | undefined { const translate = `translate(${compartment.bounds.x}, ${compartment.bounds.y})`; const vnode: any = ( - + + {compartment.args?.identifier && ( + + )} {context.renderChildren(compartment) as ReactNode} ) as any; diff --git a/packages/glsp-client/style/diagram.css b/packages/glsp-client/style/diagram.css index b29e058e..d1d88ec2 100644 --- a/packages/glsp-client/style/diagram.css +++ b/packages/glsp-client/style/diagram.css @@ -96,6 +96,13 @@ baseline-shift: 2px; } +.sprotty line.identifier { + stroke: black; + stroke-width: 0.75px; + stroke-dasharray: none; + stroke-linecap: round; +} + .sprotty[id^='system-diagram'] .tool-palette { top: 11px; right: 11px; @@ -131,3 +138,8 @@ .command-palette { animation: none; } + +.attribute.identifier .icon-path { + transform: scale(0.0105) translate(-400px, 1375px); + fill: #000; +} diff --git a/packages/protocol/src/model-service/protocol.ts b/packages/protocol/src/model-service/protocol.ts index e0409fde..f2a996e4 100644 --- a/packages/protocol/src/model-service/protocol.ts +++ b/packages/protocol/src/model-service/protocol.ts @@ -54,6 +54,7 @@ export interface Attribute extends CrossModelElement, Identifiable { export const EntityAttributeType = 'EntityAttribute'; export interface EntityAttribute extends Attribute { readonly $type: typeof EntityAttributeType; + identifier?: boolean; } export const RelationshipType = 'Relationship'; diff --git a/packages/react-model-ui/src/views/common/EntityAttributesDataGrid.tsx b/packages/react-model-ui/src/views/common/EntityAttributesDataGrid.tsx index ca94c836..cb7d8bf7 100644 --- a/packages/react-model-ui/src/views/common/EntityAttributesDataGrid.tsx +++ b/packages/react-model-ui/src/views/common/EntityAttributesDataGrid.tsx @@ -2,11 +2,14 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ import { EntityAttribute, EntityAttributeType } from '@crossbreeze/protocol'; +import CheckBoxOutlineBlankOutlined from '@mui/icons-material/CheckBoxOutlineBlankOutlined'; +import CheckBoxOutlined from '@mui/icons-material/CheckBoxOutlined'; import { GridColDef } from '@mui/x-data-grid'; import * as React from 'react'; import { useEntity, useModelDispatch } from '../../ModelContext'; import { ErrorView } from '../ErrorView'; import GridComponent, { GridComponentRow, ValidationFunction } from './GridComponent'; +import { KeyIcon } from './Icons'; export type EntityAttributeRow = GridComponentRow; @@ -27,7 +30,8 @@ export function EntityAttributesDataGrid(): React.ReactElement { id: attribute.id, name: attribute.name, datatype: attribute.datatype, - description: attribute.description + description: attribute.description, + identifier: attribute.identifier } }); return attribute; @@ -87,7 +91,7 @@ export function EntityAttributesDataGrid(): React.ReactElement { [] ); - const columns = React.useMemo( + const columns = React.useMemo[]>( () => [ { field: 'name', @@ -100,9 +104,23 @@ export function EntityAttributesDataGrid(): React.ReactElement { field: 'datatype', headerName: 'Data type', editable: true, + flex: 100, type: 'singleSelect', valueOptions: ['Integer', 'Float', 'Char', 'Varchar', 'Bool'] }, + { + field: 'identifier', + renderHeader: () => , + renderCell: ({ row }) => + row.identifier ? ( + + ) : ( + + ), + maxWidth: 50, + editable: true, + type: 'boolean' + }, { field: 'description', headerName: 'Description', editable: true, flex: 200 } ], [] @@ -124,7 +142,7 @@ export function EntityAttributesDataGrid(): React.ReactElement { return ; } return ( - autoHeight gridColumns={columns} gridData={entity.attributes} diff --git a/packages/react-model-ui/src/views/common/Icons.tsx b/packages/react-model-ui/src/views/common/Icons.tsx new file mode 100644 index 00000000..fd810265 --- /dev/null +++ b/packages/react-model-ui/src/views/common/Icons.tsx @@ -0,0 +1,16 @@ +/******************************************************************************** + * Copyright (c) 2024 CrossBreeze. + ********************************************************************************/ +/* eslint-disable max-len */ + +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; +import React = require('react'); + +export const KeyIcon: React.FC = props => ( + // Exported from https://fonts.google.com/icons?selected=Material+Symbols+Outlined:key_vertical:FILL@0;wght@400;GRAD@0;opsz@48&icon.query=key&icon.size=null&icon.color=%235f6368 + // It seems that the browser scales nicer if we do not provide a size in the SVG export + // ViewBox property comes from the export + + + +);