Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unrestrict node connections #291

Merged
merged 3 commits into from
Dec 18, 2023
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
5 changes: 5 additions & 0 deletions src/common/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export const nodeSchema = z.object({
topicId: z.number(),
arguedDiagramPartId: z.string().uuid().nullable(),
type: zNodeTypes,
customType: z
.string()
.max(30)
.regex(/^[a-z ]+$/i)
.optional(),
text: z.string().max(200),
notes: z.string().max(10000),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN;

-- AlterTable
ALTER TABLE "nodes" DROP COLUMN "customType";

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN;

-- AlterTable
ALTER TABLE "nodes" ADD COLUMN "customType" VARCHAR(30);

COMMIT;
1 change: 1 addition & 0 deletions src/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ model Node {
topicId Int
arguedDiagramPartId String? @db.Uuid // only set if this is a claim node; ideally this would be FK but could point to either Node or Edge
type NodeType
customType String? @db.VarChar(30) // arbitrary max, "Solution Component" is 18 chars
text String @db.VarChar(200) // arbitrary max, ~50 chars fit on 3 lines of the node's text area
notes String @default("") @db.VarChar(10000) // arbitrarily large max length
createdAt DateTime @default(now())
Expand Down
17 changes: 15 additions & 2 deletions src/web/topic/components/Node/EditableNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { useEffect, useRef } from "react";

import { useSessionUser } from "../../../common/hooks";
import { openContextMenu } from "../../../common/store/contextMenuActions";
import { useUnrestrictedEditing } from "../../../view/actionConfigStore";
import { setSelected, useIsGraphPartSelected } from "../../../view/navigateStore";
import { finishAddingNode, setNodeLabel } from "../../store/actions";
import { finishAddingNode, setCustomNodeType, setNodeLabel } from "../../store/actions";
import { useUserCanEditTopicData } from "../../store/userHooks";
import { Node } from "../../utils/graph";
import { nodeDecorations } from "../../utils/node";
Expand Down Expand Up @@ -35,6 +36,7 @@ interface Props {
export const EditableNode = ({ node, supplemental = false, className = "" }: Props) => {
const { sessionUser } = useSessionUser();
const userCanEditTopicData = useUserCanEditTopicData(sessionUser?.username);
const unrestrictedEditing = useUnrestrictedEditing();
const selected = useIsGraphPartSelected(node.id);

const theme = useTheme();
Expand Down Expand Up @@ -64,6 +66,7 @@ export const EditableNode = ({ node, supplemental = false, className = "" }: Pro
const nodeDecoration = nodeDecorations[node.type];
const color = theme.palette[node.type].main;
const NodeIcon = nodeDecoration.NodeIcon;
const typeText = node.data.customType ?? nodeDecoration.title;

// Require selecting a node before editing it, because oftentimes you'll want to select a node to
// view more details, and the editing will be distracting. Only edit after clicking when selected.
Expand All @@ -80,7 +83,17 @@ export const EditableNode = ({ node, supplemental = false, className = "" }: Pro
<YEdgeDiv>
<NodeTypeDiv>
<NodeIcon sx={{ width: "0.875rem", height: "0.875rem" }} />
<NodeTypeSpan>{nodeDecoration.title}</NodeTypeSpan>
<NodeTypeSpan
contentEditable={userCanEditTopicData && unrestrictedEditing}
suppressContentEditableWarning // https://stackoverflow.com/a/49639256/8409296
onBlur={(event) => {
if (event.target.textContent && event.target.textContent !== node.data.customType)
setCustomNodeType(node, event.target.textContent.trim());
}}
className="nopan"
>
{typeText}
</NodeTypeSpan>
</NodeTypeDiv>
<NodeIndicatorGroup node={node} />
</YEdgeDiv>
Expand Down
16 changes: 16 additions & 0 deletions src/web/topic/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import set from "lodash/set";

import { edgeSchema } from "../../../common/edge";
import { errorWithData } from "../../../common/errorHandling";
import { nodeSchema } from "../../../common/node";
import {
Edge,
GraphPart,
Expand Down Expand Up @@ -63,6 +64,21 @@ export const setNodeLabel = (node: Node, value: string) => {
useTopicStore.setState(finishDraft(state), false, "setNodeLabel");
};

export const setCustomNodeType = (node: Node, value: string) => {
const state = createDraft(useTopicStore.getState());

if (!nodeSchema.shape.customType.parse(value))
throw errorWithData("label should only contain alphaspace", value);

const foundNode = findNode(node.id, state.nodes);

/* eslint-disable functional/immutable-data, no-param-reassign */
foundNode.data.customType = value;
/* eslint-enable functional/immutable-data, no-param-reassign */

useTopicStore.setState(finishDraft(state), false, "setCustomEdgeLabel");
};

export const setCustomEdgeLabel = (edge: Edge, value: string) => {
const state = createDraft(useTopicStore.getState());

Expand Down
2 changes: 2 additions & 0 deletions src/web/topic/utils/apiConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const convertToStoreNode = (apiNode: TopicNode) => {
notes: apiNode.notes,
type: apiNode.type,
arguedDiagramPartId: apiNode.arguedDiagramPartId ?? undefined,
customType: apiNode.customType ?? undefined,
});
};

Expand Down Expand Up @@ -56,6 +57,7 @@ export const convertToApiNode = (storeNode: StoreNode, topicId: number): ApiNode
topicId: topicId,
arguedDiagramPartId: storeNode.data.arguedDiagramPartId ?? null,
type: storeNode.type,
customType: storeNode.data.customType,
text: storeNode.data.label,
notes: storeNode.data.notes,
};
Expand Down
17 changes: 9 additions & 8 deletions src/web/topic/utils/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,15 @@ export const getRelation = (
child: NodeType,
relationName?: RelationName
): Relation | undefined => {
if (relationName) {
return relations.find(
(relation) =>
relation.parent === parent && relation.child === child && relation.name === relationName
);
} else {
return relations.find((relation) => relation.parent === parent && relation.child === child);
}
const relation = relationName
? relations.find(
(relation) =>
relation.parent === parent && relation.child === child && relation.name === relationName
)
: relations.find((relation) => relation.parent === parent && relation.child === child);

// we're assuming that anything can relate to anything else; potentially this should only be true when unrestricted editing is on
return relation ?? { child, name: "relatesTo", parent };
};

export const composedRelations: Relation[] = [
Expand Down
27 changes: 20 additions & 7 deletions src/web/topic/utils/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export interface Graph {
export interface Node {
id: string;
data: {
/**
* Distinguished from `type` because this is explicitly open user input, and `type` can maintain stricter typing
*/
customType?: string;
label: string;
notes: string;
arguedDiagramPartId?: string;
Expand All @@ -30,15 +34,24 @@ export interface ProblemNode extends Node {

interface BuildProps {
id?: string;
customType?: string;
label?: string;
notes?: string;
type: FlowNodeType;
arguedDiagramPartId?: string;
}
export const buildNode = ({ id, label, notes, type, arguedDiagramPartId }: BuildProps): Node => {
export const buildNode = ({
id,
customType,
label,
notes,
type,
arguedDiagramPartId,
}: BuildProps): Node => {
const node = {
id: id ?? uuid(),
data: {
customType: customType,
label: label ?? `new node`,
notes: notes ?? "",
arguedDiagramPartId: arguedDiagramPartId,
Expand All @@ -59,12 +72,12 @@ export type RelationDirection = "parent" | "child";
export interface Edge {
id: string;
data: {
arguedDiagramPartId?: string;
notes: string;
/**
* Distinguished from `label` because this is explicitly open user input, and `label` can maintain stricter typing
*/
customLabel?: string;
notes: string;
arguedDiagramPartId?: string;
};
label: RelationName;
markerStart: { type: MarkerType; width: number; height: number };
Expand All @@ -91,28 +104,28 @@ export interface Edge {

interface BuildEdgeProps {
id?: string;
customLabel?: string;
notes?: string;
sourceId: string;
targetId: string;
relation: RelationName;
arguedDiagramPartId?: string;
customLabel?: string;
}
export const buildEdge = ({
id,
customLabel,
notes,
sourceId,
targetId,
relation,
arguedDiagramPartId,
customLabel,
}: BuildEdgeProps): Edge => {
return {
id: id ?? uuid(),
data: {
arguedDiagramPartId: arguedDiagramPartId,
notes: notes ?? "",
customLabel: customLabel,
notes: notes ?? "",
arguedDiagramPartId: arguedDiagramPartId,
},
label: relation,
markerStart: markerStart,
Expand Down
Loading