Skip to content

Commit

Permalink
Add button that creates new attribute to the dnd dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
RadStr committed Jan 29, 2025
1 parent 9b88336 commit 513c37c
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import { removeAttributesFromVisualModelAction } from "./remove-attribute-from-v
import { addSemanticAttributeToVisualModelAction } from "./add-semantic-attribute-to-visual-model";
import { ShiftAttributeDirection, shiftAttributePositionAction } from "./shift-attribute";
import { openEditNodeAttributesDialogAction } from "./open-edit-node-attributes-dialog";
import { EditAttributeDialogState } from "../dialog/attribute/edit-attribute-dialog-controller";
import { EditAttributeProfileDialogState } from "../dialog/attribute-profile/edit-attribute-profile-dialog-controller";

const LOG = createLogger(import.meta.url);

Expand Down Expand Up @@ -95,7 +97,19 @@ interface DialogActions {
* Opens dialog, which purpose is to create new attribute in model identified by {@link model}.
* @param model is the identifier of the semantic model.
*/
openCreateAttributeDialog: (model: string) => void;
openCreateAttributeDialogForModel: (model: string) => void;

/**
* Opens dialog, which purpose is to create new attribute with domain class identified by {@link classIdentifier}.
* On successful creation {@link onConfirmCallback} is called.
* @param classIdentifier is the identifier of the class, which will be domain for the attribute.
* @param onConfirmCallback This callback is called after we sucessfully create attribute.
* Set to null, if there is no callback.
*/
openCreateAttributeDialogForClass: (
classIdentifier: string,
onConfirmCallback: ((state: EditAttributeDialogState | EditAttributeProfileDialogState, createdAttributeIdentifier: string) => void) | null
) => void;

// TODO RadStr: Document
openEditNodeAttributesDialog: (nodeIdentifier: string) => void;
Expand Down Expand Up @@ -265,7 +279,8 @@ const noOperationActionsContext = {
openModifyDialog: noOperation,
openCreateClassDialog: noOperation,
openCreateAssociationDialog: noOperation,
openCreateAttributeDialog: noOperation,
openCreateAttributeDialogForModel: noOperation,
openCreateAttributeDialogForClass: noOperation,
openEditNodeAttributesDialog: noOperation,
openCreateProfileDialog: noOperation,
//
Expand Down Expand Up @@ -475,11 +490,14 @@ function createActionsContext(
});
}

const addAttributeForNode = (identifier: string) => {
const openCreateAttributeDialogForClass = (
classIdentifier: string,
onConfirmCallback: ((state: EditAttributeDialogState | EditAttributeProfileDialogState, createdAttributeIdentifier: string) => void) | null
) => {
withVisualModel(notifications, graph, (visualModel) => {
openCreateAttributeForEntityDialogAction(
options, dialogs, classes, graph, notifications,
visualModel, identifier);
visualModel, classIdentifier, onConfirmCallback);
});
};

Expand Down Expand Up @@ -525,7 +543,7 @@ function createActionsContext(
}
};

const openCreateAttributeDialog = (model: string) => {
const openCreateAttributeDialogForModel = (model: string) => {
const visualModel = graph.aggregatorView.getActiveVisualModel();
const modelInstance = graph.models.get(model);
if (modelInstance === null || modelInstance instanceof InMemorySemanticModel) {
Expand Down Expand Up @@ -777,7 +795,7 @@ function createActionsContext(

onChangeWaypointPositions: changeWaypointPositions,

onAddAttributeForNode: (node) => addAttributeForNode(node.externalIdentifier),
onAddAttributeForNode: (node) => openCreateAttributeDialogForClass(node.externalIdentifier, null),

onCreateConnectionToNode: (source, target) => {
openCreateConnectionDialog(source.externalIdentifier, target.externalIdentifier);
Expand Down Expand Up @@ -887,7 +905,8 @@ function createActionsContext(
openModifyDialog,
openCreateClassDialog,
openCreateAssociationDialog,
openCreateAttributeDialog,
openCreateAttributeDialogForModel,
openCreateAttributeDialogForClass,
openEditNodeAttributesDialog,
openCreateProfileDialog,
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function openCreateAttributeForEntityDialogAction(
notifications: UseNotificationServiceWriterType,
visualModel: VisualModel | null,
identifier: string,
onConfirmCallback: ((state: EditAttributeDialogState | EditAttributeProfileDialogState, createdAttributeIdentifier: string) => void) | null,
) {
const aggregate = graph.aggregatorView.getEntities()?.[identifier];

Expand All @@ -48,6 +49,12 @@ export function openCreateAttributeForEntityDialogAction(
if(visualModel !== null && isWritableVisualModel(visualModel)) {
addSemanticAttributeToVisualModelAction(notifications, visualModel, state.domain.identifier, result?.identifier ?? null, null);
}

if(onConfirmCallback !== null) {
if(result !== null) {
onConfirmCallback(state, result.identifier);
}
}
};
const state = createAddAttributeDialogState(
classes, graph, visualModel, options.language, entity);
Expand All @@ -60,7 +67,12 @@ export function openCreateAttributeForEntityDialogAction(
return;
}
if (isSemanticModelRelationship(profiled) || isSemanticModelRelationshipUsage(profiled)) {
createRelationshipProfile(state, graph.models, profiled);
const result = createRelationshipProfile(state, graph.models, profiled);
if(onConfirmCallback !== null) {
if(result !== null) {
onConfirmCallback(state, result.identifier);
}
}
} else {
notifications.error("Invalid entity to profile.");
}
Expand Down Expand Up @@ -171,4 +183,4 @@ const createRelationshipProfile = (
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function openEditNodeAttributesDialogAction(
// .map(relationship => ({identifier: relationship.id, name: relationship.name[options.language]}));
const { visibleAttributes, hiddenAttributes } = splitIntoVisibleAndHiddenAttributes(classes.rawEntities, node, options.language);

dialogs.openDialog(createEditClassAttributesDialog(onConfirm, visibleAttributes, hiddenAttributes));
dialogs.openDialog(createEditClassAttributesDialog(onConfirm, visibleAttributes, hiddenAttributes, node.representedEntity, options.language));
}

type VisibleAnHiddenAttributes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ function renderAddButton(actions: ActionsContextType, type: EntityType, model: E
actions.openCreateClassDialog(model.getId());
break;
case EntityType.Attribute:
actions.openCreateAttributeDialog(model.getId());
actions.openCreateAttributeDialogForModel(model.getId());
break;
case EntityType.Relationship:
actions.openCreateAssociationDialog(model.getId());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { type DialogProps } from "../dialog-api";
import { Language } from "../../application/options";

export type IdentifierAndName = {
identifier: string,
Expand All @@ -9,38 +10,48 @@ export type IdentifierAndName = {
export interface EditNodeAttributesState {
visibleAttributes: IdentifierAndName[];
hiddenAttributes: IdentifierAndName[];
classIdentifier: string,
language: Language,
}

export function createEditNodeAttributesState(
visibleAttributes: IdentifierAndName[],
hiddenAttributes: IdentifierAndName[],
classIdentifier: string,
language: Language,
): EditNodeAttributesState {
return {
visibleAttributes,
hiddenAttributes
hiddenAttributes,
classIdentifier,
language,
};
}

// Name them explicitly, because when we use Omit, the TS can't interfere it being typeof.
export type EditNodeAttributeChangeablePartOfState = "visibleAttributes" | "hiddenAttributes";

export interface CreateEditNodeAttributesControllerType {
moveToNewPosition: (
sourceFieldInState: keyof EditNodeAttributesState,
targetFieldInState: keyof EditNodeAttributesState,
sourceFieldInState: EditNodeAttributeChangeablePartOfState,
targetFieldInState: EditNodeAttributeChangeablePartOfState,
oldPosition: number,
newPosition: number
) => void;
addToVisibleAttributes: (newAttribute: IdentifierAndName) => void;
}

export function useEditNodeAttributesController({ state, changeState }: DialogProps<EditNodeAttributesState>): CreateEditNodeAttributesControllerType {
return useMemo(() => {

const moveToNewPosition = (
sourceFieldInState: keyof EditNodeAttributesState,
targetFieldInState: keyof EditNodeAttributesState,
sourceFieldInState: EditNodeAttributeChangeablePartOfState,
targetFieldInState: EditNodeAttributeChangeablePartOfState,
oldPosition: number,
newPosition: number
) => {
const isSourceSameAsTarget = sourceFieldInState === targetFieldInState;
const nextStateForSourceFieldInState = [...state[sourceFieldInState]];
const nextStateForSourceFieldInState: IdentifierAndName[] = [...state[sourceFieldInState]];
const nextStateForTargetFieldInState = isSourceSameAsTarget ? nextStateForSourceFieldInState : [...state[targetFieldInState]];
const [removed] = nextStateForSourceFieldInState.splice(oldPosition, 1);
nextStateForTargetFieldInState.splice(newPosition, 0, removed);
Expand All @@ -52,8 +63,18 @@ export function useEditNodeAttributesController({ state, changeState }: DialogPr
changeState(nextState);
};

const addToVisibleAttributes = (newAttribute: IdentifierAndName) => {
const nextState: EditNodeAttributesState = {
...state,
visibleAttributes: state.visibleAttributes.concat(newAttribute),
};

changeState(nextState);
};

return {
moveToNewPosition,
addToVisibleAttributes,
};
}, [state, changeState]);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { useActions } from "../../action/actions-react-binding";
import { t } from "../../application";
import { Language } from "../../application/options";
import { getStringFromLanguageStringInLang } from "../../util/language-utils";
import { EditAttributeProfileDialogState } from "../attribute-profile/edit-attribute-profile-dialog-controller";
import { EditAttributeDialogState } from "../attribute/edit-attribute-dialog-controller";
import { DialogProps, DialogWrapper } from "../dialog-api";
import { createEditNodeAttributesState, EditNodeAttributesState, IdentifierAndName, useEditNodeAttributesController } from "./edit-node-attributes-dialog-controller";
import { createEditNodeAttributesState, EditNodeAttributeChangeablePartOfState, EditNodeAttributesState, IdentifierAndName, useEditNodeAttributesController } from "./edit-node-attributes-dialog-controller";
// TODO RadStr: Drag-drop Newly also in the dependencies of other editor (kinda funny that the upgrade happened literally at the same time)
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";

Expand All @@ -8,7 +14,7 @@ const dropContextsIdentifiers = [
"invisible-attributes",
] as const;

const dropContextsIdentifiersToFieldMap: Record<typeof dropContextsIdentifiers[number], keyof EditNodeAttributesState> = {
const dropContextsIdentifiersToFieldMap: Record<typeof dropContextsIdentifiers[number], EditNodeAttributeChangeablePartOfState> = {
"visible-attributes": "visibleAttributes",
"invisible-attributes": "hiddenAttributes",
};
Expand All @@ -17,11 +23,13 @@ export const createEditClassAttributesDialog = (
onConfirm: ((state: EditNodeAttributesState) => void) | null,
visibleAttributes: IdentifierAndName[],
hiddenAttributes: IdentifierAndName[],
classIdentifier: string,
language: Language
): DialogWrapper<EditNodeAttributesState> => {
return {
label: "edit-class-attributes-dialog.label",
component: CreateEditNodeAttributesDialog,
state: createEditNodeAttributesState(visibleAttributes, hiddenAttributes),
state: createEditNodeAttributesState(visibleAttributes, hiddenAttributes, classIdentifier, language),
confirmLabel: "edit-class-attributes-dialog.btn-ok",
cancelLabel: "edit-class-attributes-dialog.btn-cancel",
validate: null,
Expand All @@ -34,7 +42,11 @@ export const createEditClassAttributesDialog = (
export const CreateEditNodeAttributesDialog = (props: DialogProps<EditNodeAttributesState>) => {
const state = props.state;
const controller = useEditNodeAttributesController(props);
// TODO RadStr: Not sure - maybe should also access in controller?
const actions = useActions();

// TODO RadStr: Maybe should be part of controller, but then I need to export the dropContextsIdentifiersToFieldMap and dropContextsIdentifiers,
// Same for others
const handleDragEnd = (result: DropResult) => {
if (!result.destination) {
return; // If dropped outside a valid drop zone
Expand All @@ -50,12 +62,25 @@ export const CreateEditNodeAttributesDialog = (props: DialogProps<EditNodeAttrib
const showAttribute = (index: number) => {
controller.moveToNewPosition("hiddenAttributes", "visibleAttributes", index, 0);
};
const onCreateNewAttribute = () => {
const onConfirmCallback = (state: EditAttributeDialogState | EditAttributeProfileDialogState, createdAttributeIdentifier: string) => {
const name = getStringFromLanguageStringInLang(state.name, state.language)[0] ?? createdAttributeIdentifier;
// We have to use timeout - there is probably some issue with updating state of multiple dialogs when one closes.
setTimeout(() => controller.addToVisibleAttributes({
identifier: createdAttributeIdentifier,
name
}), 1);
}
actions.openCreateAttributeDialogForClass(state.classIdentifier, onConfirmCallback);
}

// TODO RadStr: Once finalized - use localization for the name
return <div className="flex flex-row">
<DragDropContext onDragEnd={handleDragEnd}>
<DroppableArea name="Visible attributes:" dropContextIdentifier={dropContextsIdentifiers[0]} state={state} hideAttribute={hideAttribute} showAttribute={null}></DroppableArea>
<DroppableArea name="Hidden attributes:" dropContextIdentifier={dropContextsIdentifiers[1]} state={state} hideAttribute={null} showAttribute={showAttribute}></DroppableArea>
<DroppableArea name="Visible attributes:" dropContextIdentifier={dropContextsIdentifiers[0]}
state={state} hideAttribute={hideAttribute} showAttribute={null} onCreateNewAttribute={onCreateNewAttribute}></DroppableArea>
<DroppableArea name="Hidden attributes:" dropContextIdentifier={dropContextsIdentifiers[1]}
state={state} hideAttribute={null} showAttribute={showAttribute} onCreateNewAttribute={null}></DroppableArea>
</DragDropContext>
<SimpleHorizontalLineSeparator/>
</div>;
Expand All @@ -68,6 +93,7 @@ type DroppableAreaProps = {
dropContextIdentifier: typeof dropContextsIdentifiers[number],
hideAttribute: ((index: number) => void) | null,
showAttribute: ((index: number) => void) | null,
onCreateNewAttribute: (() => void) | null,
}

const getDroppableAreaStyle = (isDraggingOver: boolean): React.CSSProperties => ({
Expand All @@ -82,7 +108,12 @@ const DroppableArea = (props: DroppableAreaProps) => {
const fieldInState = dropContextsIdentifiersToFieldMap[props.dropContextIdentifier];

return <div style={{ flex: 1 }}>
<p className="font-bold">{props.name}</p>
<p className="font-bold">
{props.name}
{props.onCreateNewAttribute === null ?
null :
<button title={t("node-add-attribute")}onClick={props.onCreateNewAttribute}></button>}
</p>
<Droppable droppableId={props.dropContextIdentifier}>
{(provided, snapshot) => (
<div
Expand Down

0 comments on commit 513c37c

Please sign in to comment.