Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
* Change CSS class for default link template from `reactodia-default-link` to `reactodia-standard-link`.
- Move "expand/collapse on double click" global element behavior to `StandardEntity` and `ClassicEntity` implementation only.
- Add `setTemplateProperty()` utility function to easily set or unset template state property.
- Change `MetadataProvider.{createEntity, createRelation}` to return result object with initial template state in addition to the data to customize the created cells (i.e. new elements can be expanded or collapsed).
- Add `EditorController.applyAuthoringChanges()` method to apply current authoring changes to the diagram (i.e. change entity data, delete relations, etc) and reset the change state to be empty.
- Deprecate and hide by default "Edit" and "Delete" action buttons in `StandardEntity` expanded state (can be re-enabled by setting `showActions` prop to `true`).

#### 🐛 Fixed
- Fix `HaloLink` and visual authoring link path highlight being rendered on top on elements by placing it onto `overLinkGeometry` widget layer instead.
Expand Down
27 changes: 17 additions & 10 deletions examples/resources/exampleMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,29 @@ export class ExampleMetadataProvider extends Reactodia.BaseMetadataProvider {
.toString(16).substring(1);
const typeLabel = Reactodia.Rdf.getLocalName(type) ?? 'Entity';
return {
id: `${type}_${random32BitDigits}`,
types: [type],
properties: {
[Reactodia.rdfs.label]: [
Reactodia.Rdf.DefaultDataFactory.literal(`New ${typeLabel}`)
]
data: {
id: `${type}_${random32BitDigits}`,
types: [type],
properties: {
[Reactodia.rdfs.label]: [
Reactodia.Rdf.DefaultDataFactory.literal(`New ${typeLabel}`)
]
},
},
elementState: type === owl.Class ? {
[Reactodia.TemplateProperties.Expanded]: true,
} : undefined,
};
},
createRelation: async (source, target, linkType, {signal}) => {
await Reactodia.delay(SIMULATED_DELAY, {signal: signal});
return {
sourceId: source.id,
targetId: target.id,
linkTypeId: linkType,
properties: {},
data: {
sourceId: source.id,
targetId: target.id,
linkTypeId: linkType,
properties: {},
},
};
},
canConnect: async (source, target, linkType, {signal}) => {
Expand Down
38 changes: 27 additions & 11 deletions src/data/metadataProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ElementTemplateState, LinkTemplateState } from '../diagram/elements';

import type * as Rdf from './rdf/rdfModel';
import type {
ElementModel, ElementTypeIri, LinkTypeIri, PropertyTypeIri, LinkModel,
Expand All @@ -20,14 +22,14 @@ export interface MetadataProvider {
createEntity(
type: ElementTypeIri,
options: { readonly signal?: AbortSignal }
): Promise<ElementModel>;
): Promise<MetadataCreatedEntity>;

createRelation(
source: ElementModel,
target: ElementModel,
linkType: LinkTypeIri,
options: { readonly signal?: AbortSignal }
): Promise<LinkModel>;
): Promise<MetadataCreatedRelation>;

canConnect(
source: ElementModel,
Expand Down Expand Up @@ -64,6 +66,16 @@ export interface MetadataProvider {
): Promise<ReadonlySet<ElementTypeIri>>;
}

export interface MetadataCreatedEntity {
readonly data: ElementModel;
readonly elementState?: ElementTemplateState;
}

export interface MetadataCreatedRelation {
readonly data: LinkModel;
readonly linkState?: LinkTemplateState;
}

export interface MetadataCanConnect {
readonly targetTypes: ReadonlySet<ElementTypeIri>;
readonly inLinks: ReadonlyArray<LinkTypeIri>;
Expand Down Expand Up @@ -142,14 +154,16 @@ export class BaseMetadataProvider implements MetadataProvider {
async createEntity(
type: ElementTypeIri,
options: { readonly signal?: AbortSignal; }
): Promise<ElementModel> {
): Promise<MetadataCreatedEntity> {
if (this.methods.createEntity) {
return this.methods.createEntity(type, options);
}
return {
id: '',
types: [],
properties: {},
data: {
id: '',
types: [],
properties: {},
},
};
}

Expand All @@ -158,15 +172,17 @@ export class BaseMetadataProvider implements MetadataProvider {
target: ElementModel,
linkType: LinkTypeIri,
options: { readonly signal?: AbortSignal; }
): Promise<LinkModel> {
): Promise<MetadataCreatedRelation> {
if (this.methods.createRelation) {
return this.methods.createRelation(source, target, linkType, options);
}
return {
linkTypeId: linkType,
sourceId: source.id,
targetId: target.id,
properties: {},
data: {
linkTypeId: linkType,
sourceId: source.id,
targetId: target.id,
properties: {},
},
};
}

Expand Down
5 changes: 0 additions & 5 deletions src/editor/editorController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,6 @@ export class EditorController {
);

const element = model.createElement(data);
// TODO: customize initial state via MetadataProvider
element.setElementState({
...element.elementState,
[TemplateProperties.Expanded]: true,
});

if (options.temporary) {
this.setTemporaryState(
Expand Down
23 changes: 12 additions & 11 deletions src/forms/elementTypeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { EventObserver } from '../coreUtils/events';
import { Translation } from '../coreUtils/i18n';

import { PlaceholderEntityType } from '../data/schema';
import { ElementModel, ElementTypeIri } from '../data/model';
import { DataProviderLookupItem } from '../data/dataProvider';
import type { ElementModel, ElementTypeIri } from '../data/model';
import type { DataProviderLookupItem } from '../data/dataProvider';
import type { MetadataCreatedEntity } from '../data/metadataProvider';

import { HtmlSpinner } from '../diagram/spinner';

Expand All @@ -28,7 +29,7 @@ export interface ElementTypeSelectorProps {
}

export interface ElementValue {
value: ElementModel;
value: MetadataCreatedEntity;
isNew: boolean;
loading: boolean;
error?: string;
Expand Down Expand Up @@ -188,13 +189,13 @@ export class ElementTypeSelectorInner extends React.Component<ElementTypeSelecto

const {onChange, workspace: {editor: {metadataProvider}}} = this.props;
const elementTypeIri: ElementTypeIri = (e.target as HTMLSelectElement).value;
let elementModel: ElementModel | null;
let createdEntity: MetadataCreatedEntity | null;
try {
elementModel = await mapAbortedToNull(
createdEntity = await mapAbortedToNull(
metadataProvider!.createEntity(elementTypeIri, {signal}),
signal
);
if (elementModel === null) {
if (createdEntity === null) {
return;
}
} catch (err) {
Expand All @@ -205,7 +206,7 @@ export class ElementTypeSelectorInner extends React.Component<ElementTypeSelecto

this.setState({elementTypesState: 'none'});
onChange({
value: elementModel,
value: createdEntity,
isNew: true,
loading: false,
});
Expand All @@ -228,7 +229,7 @@ export class ElementTypeSelectorInner extends React.Component<ElementTypeSelecto
private renderElementTypeSelector() {
const {elementValue, workspace: {translation: t}} = this.props;
const {elementTypes, elementTypesState} = this.state;
const value = elementValue.value.types.length ? elementValue.value.types[0] : '';
const value = elementValue.value.data.types.length ? elementValue.value.data.types[0] : '';
if (elementTypesState !== 'none') {
return (
<HtmlSpinner width={20} height={20}
Expand Down Expand Up @@ -283,7 +284,7 @@ export class ElementTypeSelectorInner extends React.Component<ElementTypeSelecto
<ListElementView key={element.id}
element={element}
disabled={isAlreadyOnDiagram || !hasAppropriateType}
selected={element.id === elementValue.value.id}
selected={element.id === elementValue.value.data.id}
onClick={(e, model) => void this.onSelectExistingItem(model)}
/>
);
Expand All @@ -299,12 +300,12 @@ export class ElementTypeSelectorInner extends React.Component<ElementTypeSelecto
this.loadingItemCancellation = new AbortController();
const signal = this.loadingItemCancellation.signal;

onChange({value: data, isNew: false, loading: true});
onChange({value: {data}, isNew: false, loading: true});
const result = await model.dataProvider.elements({elementIds: [data.id]});
if (signal.aborted) { return; }

const loadedModel = result.get(data.id)!;
onChange({value: loadedModel, isNew: false, loading: false});
onChange({value: {data: loadedModel}, isNew: false, loading: false});
}

render() {
Expand Down
18 changes: 7 additions & 11 deletions src/forms/findOrCreateEntityForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class FindOrCreateEntityForm extends React.Component<FindOrCreateEntityFo
this.link = originalLink;
this.state = {
elementValue: {
value: target.data,
value: {data: target.data},
isNew: initialTargetIsNew,
loading: false,
validated: true,
Expand Down Expand Up @@ -85,7 +85,7 @@ export class FindOrCreateEntityForm extends React.Component<FindOrCreateEntityFo
this.validationCancellation = new AbortController();
const signal = this.validationCancellation.signal;

const validateElement = validateElementType(elementValue.value, this.context);
const validateElement = validateElementType(elementValue.value.data, this.context);
const validateLink = validateLinkType(
dataFromExtendedLink(linkValue.link),
originalLink.data,
Expand Down Expand Up @@ -126,7 +126,7 @@ export class FindOrCreateEntityForm extends React.Component<FindOrCreateEntityFo
link: {
base: originalLink.data,
source: source.data,
target: newState.value,
target: newState.value.data,
direction: 'out',
},
validated: false,
Expand Down Expand Up @@ -198,7 +198,7 @@ export class FindOrCreateEntityForm extends React.Component<FindOrCreateEntityFo
editor.setTemporaryState(temporaryState);
// Target IRI change may also update link data
const batch = model.history.startBatch();
batch.history.execute(changeEntityData(model, target.iri, elementValue.value));
batch.history.execute(changeEntityData(model, target.iri, elementValue.value.data));
batch.discard();

temporaryState = TemporaryState.addEntity(temporaryState, target.data);
Expand All @@ -222,7 +222,7 @@ export class FindOrCreateEntityForm extends React.Component<FindOrCreateEntityFo
if (!elementValue.isNew) {
editor.setTemporaryState(TemporaryState.removeEntity(
editor.temporaryState,
elementValue.value
elementValue.value.data
));
}
editor.removeTemporaryCells([target, link]);
Expand All @@ -235,16 +235,12 @@ export class FindOrCreateEntityForm extends React.Component<FindOrCreateEntityFo

if (elementValue.isNew) {
model.addElement(target);
// TODO: customize initial state via MetadataProvider
target.setElementState({
...target.elementState,
[TemplateProperties.Expanded]: true,
});
target.setElementState(elementValue.value.elementState);
editor.setAuthoringState(
AuthoringState.addEntity(editor.authoringState, target.data)
);
} else {
void model.requestLinks({addedElements: [elementValue.value.id]});
void model.requestLinks({addedElements: [elementValue.value.data.id]});
}

const linkBase = relationFromExtendedLink(linkValue.link, source, target);
Expand Down
22 changes: 16 additions & 6 deletions src/templates/standardTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,17 @@ export const StandardTemplate: ElementTemplate = {
*
* @see {@link StandardEntity}
*/
export interface StandardEntityProps extends TemplateProps {}
export interface StandardEntityProps extends TemplateProps {
/**
* When set to `true`, allows to edite or delete the entity
* using corresponding buttons in the expanded state.
*
* @default false
* @deprecated Edities can be editied or deleted via inline action decorators
* when {@link VisualAuthoringProps.inlineEntityActions} is enabled.
*/
showActions?: boolean;
}

const CLASS_NAME = 'reactodia-standard-template';

Expand All @@ -67,14 +77,11 @@ const CLASS_NAME = 'reactodia-standard-template';
* - {@link TemplateProperties.Expanded}
* - {@link TemplateProperties.PinnedProperties}
*
* Entities can be edited or deleted using corresponding buttons
* from the expanded state.
*
* @category Components
* @see {@link StandardTemplate}
*/
export function StandardEntity(props: StandardEntityProps) {
const {element, isExpanded, elementState} = props;
const {showActions, element, isExpanded, elementState} = props;
const workspace = useWorkspace();
const {model, editor, translation: t, getElementTypeStyle} = workspace;

Expand Down Expand Up @@ -226,7 +233,7 @@ export function StandardEntity(props: StandardEntityProps) {
<div className={`${CLASS_NAME}__dropdown-content`}>
{renderIri(data)}
<PropertyList data={data} />
{editor.inAuthoringMode ? <>
{showActions && editor.inAuthoringMode ? <>
<hr className={`${CLASS_NAME}__hr`}
data-reactodia-no-export='true'
/>
Expand Down Expand Up @@ -521,6 +528,9 @@ function PropertyList(props: {
);
}

/**
* @deprecated
*/
function Actions(props: {
target: Element;
entityContext: AuthoredEntityContext;
Expand Down
9 changes: 5 additions & 4 deletions src/widgets/classTree/classTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,17 +418,18 @@ class ClassTreeInner extends React.Component<ClassTreeInnerProps, State> {
this.createElementCancellation = new AbortController();
const signal = this.createElementCancellation.signal;

const elementModel = await mapAbortedToNull(
const createdEntity = await mapAbortedToNull(
editor.metadataProvider!.createEntity(elementType, {signal}),
signal
);
if (elementModel === null) {
if (createdEntity === null) {
return;
}

const element = editor.createEntity(elementModel);
let createAt: [CanvasApi, Vector] | undefined;
const element = editor.createEntity(createdEntity.data);
element.setElementState(createdEntity.elementState);

let createAt: [CanvasApi, Vector] | undefined;
if (dropEvent) {
createAt = [dropEvent.source, dropEvent.position];
} else {
Expand Down
Loading