Skip to content

Commit

Permalink
Merge pull request #609 from amelioro/mitigation-node
Browse files Browse the repository at this point in the history
Mitigation node
  • Loading branch information
keyserj authored Dec 16, 2024
2 parents 5c3ac35 + 87d5158 commit 700cd6e
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 22 deletions.
2 changes: 2 additions & 0 deletions src/common/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const relationNames = [
"creates",
"fulfills",
"obstacleOf",
"mitigates",

// research
"asksAbout", //question to any node
Expand Down Expand Up @@ -66,6 +67,7 @@ export const infoRelationNames: Record<InfoCategory, RelationName[]> = {
"creates",
"fulfills",
"obstacleOf",
"mitigates",
"relatesTo", // is a generic relation but currently only seems worthwhile in topic
],
research: ["asksAbout", "potentialAnswerTo", "relevantFor", "sourceOf", "mentions"],
Expand Down
8 changes: 8 additions & 0 deletions src/common/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export const nodeTypes = [
"detriment",
"solution",
"obstacle",
"mitigationComponent",
// Technically solution can act as a mitigation, but a separate node type enables us to treat
// mitigations as lesser, not-core nodes like solutions.
// Will have to keep an eye out for if this seems worth the separate node type, or if it creates
// too much confusion.
"mitigation",

// research
"question",
Expand Down Expand Up @@ -67,6 +73,8 @@ export const infoNodeTypes: Record<InfoCategory, NodeType[]> = {
"solutionComponent",
"solution",
"obstacle",
"mitigationComponent",
"mitigation",
"custom", // is a generic node but currently only seems worthwhile in topic
],
research: ["question", "answer", "fact", "source"],
Expand Down
17 changes: 17 additions & 0 deletions src/db/migrations/20241216152339_add_mitigation_node/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
BEGIN;

-- AlterEnum
CREATE TYPE "NodeType_new" AS ENUM ('problem', 'solution', 'solutionComponent', 'criterion', 'effect', 'rootClaim', 'support', 'critique', 'question', 'answer', 'fact', 'source', 'custom', 'benefit', 'detriment', 'cause', 'obstacle');
ALTER TABLE "nodes" ALTER COLUMN "type" TYPE "NodeType_new" USING ("type"::text::"NodeType_new");
ALTER TYPE "NodeType" RENAME TO "NodeType_old";
ALTER TYPE "NodeType_new" RENAME TO "NodeType";
DROP TYPE "NodeType_old";

-- AlterEnum
CREATE TYPE "EdgeType_new" AS ENUM ('causes', 'subproblemOf', 'addresses', 'accomplishes', 'contingencyFor', 'createdBy', 'has', 'criterionFor', 'creates', 'fulfills', 'obstacleOf', 'asksAbout', 'potentialAnswerTo', 'relevantFor', 'sourceOf', 'mentions', 'supports', 'critiques', 'relatesTo');
ALTER TABLE "edges" ALTER COLUMN "type" TYPE "EdgeType_new" USING ("type"::text::"EdgeType_new");
ALTER TYPE "EdgeType" RENAME TO "EdgeType_old";
ALTER TYPE "EdgeType_new" RENAME TO "EdgeType";
DROP TYPE "EdgeType_old";

COMMIT;
10 changes: 10 additions & 0 deletions src/db/migrations/20241216152339_add_mitigation_node/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
BEGIN;

-- AlterEnum
ALTER TYPE "EdgeType" ADD VALUE 'mitigates';

-- AlterEnum
ALTER TYPE "NodeType" ADD VALUE 'mitigationComponent';
ALTER TYPE "NodeType" ADD VALUE 'mitigation';

COMMIT;
3 changes: 3 additions & 0 deletions src/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ enum NodeType {
solutionComponent
solution
obstacle
mitigationComponent
mitigation
// research
question
Expand Down Expand Up @@ -143,6 +145,7 @@ enum EdgeType {
creates
fulfills
obstacleOf
mitigates
// research
asksAbout
Expand Down
3 changes: 3 additions & 0 deletions src/web/common/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* 150 answer
* 160 criterion
* 200 support
* 230 mitigation, component (desaturated)
* 240 source
* 300 problem
* 320 cause
Expand Down Expand Up @@ -187,6 +188,8 @@ const sharedPalette = {
benefit: augmentColor({ color: { main: oklchToHex("oklch(85% 0.16 130)") } }), // light-green: good thing; slightly more saturated because the color seems nicer, brighter to contrast more with other greens (mainly solution/component)
detriment: augmentColor({ color: { main: oklchToHex("oklch(85% 0.15 340)") } }), // purple-red: bad thing; slightly brighter to contrast more with cause
obstacle: augmentColor({ color: { main: oklchToHex("oklch(75% 0.15 0)") } }), // red: bad thing
mitigation: augmentColor({ color: { main: oklchToHex("oklch(75% 0.11 230)") } }), // blue: good but different than solution/support; desaturated because the hue seems to naturally be very saturated
mitigationComponent: augmentColor({ color: { main: oklchToHex("oklch(75% 0.07 230)") } }), // grey-blue: same as mitigation but with less saturation

// research
question: augmentColor({ color: { main: oklchToHex("oklch(75% 0 0)") } }), // grey: ambiguous, uncertain
Expand Down
2 changes: 2 additions & 0 deletions src/web/topic/components/Diagram/Diagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const nodeTypes: Record<FlowNodeType, ComponentType<NodeProps>> = {
benefit: buildNodeComponent("benefit"),
detriment: buildNodeComponent("detriment"),
obstacle: buildNodeComponent("obstacle"),
mitigation: buildNodeComponent("mitigation"),
mitigationComponent: buildNodeComponent("mitigationComponent"),

// research
question: buildNodeComponent("question"),
Expand Down
18 changes: 17 additions & 1 deletion src/web/topic/components/Node/AddNodeButtonGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { memo } from "react";

import { NodeType, breakdownNodeTypes } from "@/common/node";
import { AddNodeButton } from "@/web/topic/components/Node/AddNodeButton";
import { useIsMitigatableDetriment } from "@/web/topic/store/nodeTypeHooks";
import { Relation, addableRelationsFrom } from "@/web/topic/utils/edge";
import { type RelationDirection } from "@/web/topic/utils/graph";
import { Orientation } from "@/web/topic/utils/layout";
Expand All @@ -19,6 +20,7 @@ interface Props {
const AddNodeButtonGroup = memo(
({ className, fromNodeId, fromNodeType, as, orientation }: Props) => {
const unrestrictedEditing = useUnrestrictedEditing();
const isMitigatableDetriment = useIsMitigatableDetriment(fromNodeId);

const addableRelations: { toNodeType: NodeType; relation: Relation }[] =
// if unrestricted, allow adding any topic node as parent or child (shouldn't be very useful to have outside of topic nodes)
Expand All @@ -33,7 +35,21 @@ const AddNodeButtonGroup = memo(
parent: as === "parent" ? nodeType : fromNodeType,
},
}))
: addableRelationsFrom(fromNodeType, as);
: // hack to ensure that problem detriments can't be mitigated (and can be solved), and solution detriments can be mitigated (but not solved);
// this is really awkward but keeps detriment nodes from being able to have both solutions and mitigations added, which could be really confusing for users
isMitigatableDetriment && as === "child"
? addableRelationsFrom(fromNodeType, as).map(({ toNodeType, relation }) =>
toNodeType === "solution" && relation.name === "addresses"
? {
toNodeType: "mitigation",
relation: { child: "mitigation", name: "mitigates", parent: fromNodeType },
}
: {
toNodeType,
relation,
},
)
: addableRelationsFrom(fromNodeType, as);

if (addableRelations.length === 0) return <></>;

Expand Down
24 changes: 24 additions & 0 deletions src/web/topic/store/nodeTypeHooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import { useTopicStore } from "@/web/topic/store/store";
import { findGraphPartOrThrow, findNodeOrThrow } from "@/web/topic/utils/graph";

export const useIsMitigatableDetriment = (nodeId: string) => {
return useTopicStore((state) => {
try {
const node = findNodeOrThrow(nodeId, state.nodes);
// is mitigatable if it's created by a solution or mitigation
return (
node.type === "detriment" &&
state.edges.find(
(edge) =>
(edge.source === nodeId && edge.label === "creates") ||
// Rare case where detriment is below a solution; this can exist while this discussion is still unresolved https://github.com/amelioro/ameliorate/discussions/579.
// This case could technically exist for more situations e.g. detriment created by a solution component,
// but not going to spend effort on that until the mentioned discussion determines that such a case is important to cover.
(edge.target === nodeId &&
edge.label === "createdBy" &&
state.nodes.find((node) => node.id === edge.source)?.type === "solution"),
) !== undefined
);
} catch {
return false;
}
});
};

export const useQuestionDetails = (questionNodeId: string) => {
return useTopicStore((state) => {
try {
Expand Down
39 changes: 34 additions & 5 deletions src/web/topic/utils/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,14 @@ export const relations: AddableRelation[] = researchRelations.concat([
{ child: "criterion", name: "criterionFor", parent: "problem", addableFrom: "parent" },
{ child: "solutionComponent", name: "addresses", parent: "problem", addableFrom: "child" },
{ child: "solution", name: "addresses", parent: "problem", addableFrom: "both" },
{ child: "mitigationComponent", name: "addresses", parent: "problem", addableFrom: "neither" },
{ child: "mitigation", name: "addresses", parent: "problem", addableFrom: "neither" },

{ child: "cause", name: "causes", parent: "cause", addableFrom: "parent" },
{ child: "solutionComponent", name: "addresses", parent: "cause", addableFrom: "neither" },
{ child: "solution", name: "addresses", parent: "cause", addableFrom: "parent" },
{ child: "mitigationComponent", name: "addresses", parent: "cause", addableFrom: "neither" },
{ child: "mitigation", name: "addresses", parent: "cause", addableFrom: "neither" },

{ child: "criterion", name: "relatesTo", parent: "benefit", addableFrom: "neither" },
{ child: "criterion", name: "relatesTo", parent: "effect", addableFrom: "neither" },
Expand Down Expand Up @@ -94,25 +99,49 @@ export const relations: AddableRelation[] = researchRelations.concat([
{ child: "detriment", name: "relatesTo", parent: "criterion", addableFrom: "neither" },
{ child: "solutionComponent", name: "fulfills", parent: "criterion", addableFrom: "neither" },
{ child: "solution", name: "fulfills", parent: "criterion", addableFrom: "neither" },
{ child: "mitigationComponent", name: "fulfills", parent: "criterion", addableFrom: "neither" },
{ child: "mitigation", name: "fulfills", parent: "criterion", addableFrom: "neither" },

{ child: "solutionComponent", name: "creates", parent: "benefit", addableFrom: "child" },
{ child: "solution", name: "creates", parent: "benefit", addableFrom: "child" },
{ child: "solutionComponent", name: "creates", parent: "effect", addableFrom: "child" },
{ child: "solution", name: "creates", parent: "effect", addableFrom: "child" },
{ child: "solutionComponent", name: "creates", parent: "detriment", addableFrom: "child" },
{ child: "solution", name: "creates", parent: "detriment", addableFrom: "child" },

{ child: "mitigationComponent", name: "creates", parent: "benefit", addableFrom: "child" },
{ child: "mitigation", name: "creates", parent: "benefit", addableFrom: "child" },
{ child: "mitigationComponent", name: "creates", parent: "effect", addableFrom: "child" },
{ child: "mitigation", name: "creates", parent: "effect", addableFrom: "child" },
{ child: "mitigationComponent", name: "creates", parent: "detriment", addableFrom: "child" },
{ child: "mitigation", name: "creates", parent: "detriment", addableFrom: "child" },

{ child: "solutionComponent", name: "addresses", parent: "detriment", addableFrom: "neither" },
{ child: "solution", name: "addresses", parent: "detriment", addableFrom: "parent" },
{ child: "mitigationComponent", name: "mitigates", parent: "detriment", addableFrom: "neither" },
{ child: "mitigation", name: "mitigates", parent: "detriment", addableFrom: "neither" }, // there's a hack to make this relation addable instead of solution for solution detriments

{ child: "solution", name: "has", parent: "solutionComponent", addableFrom: "child" },
{ child: "solutionComponent", name: "has", parent: "solutionComponent", addableFrom: "child" },
{ child: "solution", name: "has", parent: "solutionComponent", addableFrom: "child" },
{
child: "mitigationComponent",
name: "has",
parent: "mitigationComponent",
addableFrom: "child",
},
{ child: "mitigation", name: "has", parent: "mitigationComponent", addableFrom: "child" },

{ child: "obstacle", name: "obstacleOf", parent: "solutionComponent", addableFrom: "parent" },
{ child: "obstacle", name: "obstacleOf", parent: "solution", addableFrom: "parent" },
{ child: "obstacle", name: "obstacleOf", parent: "mitigationComponent", addableFrom: "parent" },
{ child: "obstacle", name: "obstacleOf", parent: "mitigation", addableFrom: "parent" },

{ child: "solutionComponent", name: "addresses", parent: "obstacle", addableFrom: "neither" },
{ child: "solution", name: "addresses", parent: "obstacle", addableFrom: "neither" },
{ child: "mitigationComponent", name: "mitigates", parent: "obstacle", addableFrom: "neither" },
{ child: "mitigation", name: "mitigates", parent: "obstacle", addableFrom: "parent" },

{ child: "solution", name: "accomplishes", parent: "solution", addableFrom: "parent" },
{ child: "solution", name: "contingencyFor", parent: "solution", addableFrom: "neither" },
{ child: "obstacle", name: "obstacleOf", parent: "solution", addableFrom: "parent" },

{ child: "solution", name: "addresses", parent: "obstacle", addableFrom: "parent" },

// justification relations
{ child: "support", name: "supports", parent: "rootClaim", addableFrom: "parent" },
Expand Down
29 changes: 14 additions & 15 deletions src/web/topic/utils/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { NodeType, nodeTypes } from "@/common/node";
import { scalePxViaDefaultFontSize } from "@/pages/_document.page";
import { nodeHeightPx, nodeWidthPx } from "@/web/topic/components/Node/EditableNode.styles";
import { Diagram } from "@/web/topic/utils/diagram";
import { type Edge, type Node } from "@/web/topic/utils/graph";
import { edges as nodeEdges } from "@/web/topic/utils/node";
import { type Edge, type Node, ancestors, descendants } from "@/web/topic/utils/graph";

export type Orientation = "DOWN" | "UP" | "RIGHT" | "LEFT";
export const orientation: Orientation = "DOWN" as Orientation; // not constant to allow potential other orientations in the future, and keeping code that currently exists for handling "LEFT" orientation
Expand Down Expand Up @@ -64,6 +63,8 @@ const partitionOrders: { [type in NodeType]: string } = {
solutionComponent: "2",
solution: "3",
obstacle: "null",
mitigationComponent: "null",
mitigation: "null",

// research
question: "null",
Expand All @@ -82,22 +83,20 @@ const partitionOrders: { [type in NodeType]: string } = {
custom: "null",
};

const calculatePartition = (node: Node, edges: Edge[]) => {
const calculatePartition = (node: Node, diagram: Diagram) => {
if (["effect", "benefit", "detriment"].includes(node.type)) {
const edgesOfNode = nodeEdges(node, edges);
const hasParentCreatedBy = edgesOfNode.some(
(edge) => edge.label === "createdBy" && edge.target === node.id,
// could rely on just edge "createdBy" vs "creates" rather than traversing relations, but then
// mitigation effects wouldn't be distinguishable from solution effects
const createdByProblem = ancestors(node, diagram, ["createdBy"]).some(
(ancestor) => ancestor.type === "problem",
);
const hasChildCreates = edgesOfNode.some(
(edge) => edge.label === "creates" && edge.source === node.id,
const createdBySolution = descendants(node, diagram, ["creates"]).some(
(descendant) => descendant.type === "solution" || descendant.type === "solutionComponent",
);

const shouldBeAboveCriteria = hasParentCreatedBy; // effect createdBy problem
const shouldBeBelowCriteria = hasChildCreates; // solution creates effect

if (shouldBeAboveCriteria && shouldBeBelowCriteria) return "null";
else if (shouldBeAboveCriteria) return "0";
else if (shouldBeBelowCriteria) return "2";
if (createdByProblem && createdBySolution) return "null";
else if (createdByProblem) return "0";
else if (createdBySolution) return "2";
else return "null";
} else {
return partitionOrders[node.type];
Expand Down Expand Up @@ -266,7 +265,7 @@ export const layout = async (
// solutions, components, effects; we might be able to improve that situation by modeling
// each problem within a nested node. Or maybe we could just do partitioning within
// a special "problem context view" rather than in the main topic diagram view.
"elk.partitioning.partition": calculatePartition(node, edges),
"elk.partitioning.partition": calculatePartition(node, diagram),
},
};
}),
Expand Down
9 changes: 9 additions & 0 deletions src/web/topic/utils/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
QuestionMark,
ThumbDown,
ThumbUp,
VerifiedUserOutlined,
Widgets,
} from "@mui/icons-material";

Expand Down Expand Up @@ -76,6 +77,14 @@ export const nodeDecorations: Record<FlowNodeType, NodeDecoration> = {
title: "Obstacle",
NodeIcon: Fence,
},
mitigationComponent: {
title: "Component",
NodeIcon: Widgets,
},
mitigation: {
title: "Mitigation",
NodeIcon: VerifiedUserOutlined,
},

// research
question: {
Expand Down
2 changes: 1 addition & 1 deletion src/web/view/utils/infoFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ const getSolutionDetails = (
);

const descendantDetails = solutions.flatMap((solution) =>
descendants(solution, graph, ["obstacleOf", "addresses", "accomplishes", "contingencyFor"]),
descendants(solution, graph, ["createdBy", "obstacleOf", "accomplishes", "contingencyFor"]),
);

const criteriaIds = criteria.map((criterion) => criterion.id);
Expand Down

0 comments on commit 700cd6e

Please sign in to comment.