From e6007d074e5591ad83d764a08ef3bb26142ccb79 Mon Sep 17 00:00:00 2001 From: Joel Keyser Date: Mon, 16 Dec 2024 17:12:46 -0600 Subject: [PATCH 1/3] touchup: don't partition mitigation effects mitigations are smaller/lower-focus than regular solutions, and so their effects should be able to lay out wherever. --- src/web/topic/utils/layout.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/web/topic/utils/layout.ts b/src/web/topic/utils/layout.ts index 93e05d51..7a3ab9df 100644 --- a/src/web/topic/utils/layout.ts +++ b/src/web/topic/utils/layout.ts @@ -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 @@ -82,22 +81,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]; @@ -266,7 +263,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), }, }; }), From 44a0c429d680fbb2757f1c2fd519e31c5b7670f3 Mon Sep 17 00:00:00 2001 From: Joel Keyser Date: Mon, 16 Dec 2024 17:38:50 -0600 Subject: [PATCH 2/3] feat: add mitigation node type --- src/common/edge.ts | 2 + src/common/node.ts | 8 ++++ .../down.sql | 17 ++++++++ .../migration.sql | 10 +++++ src/db/schema.prisma | 3 ++ src/web/common/theme.ts | 3 ++ src/web/topic/components/Diagram/Diagram.tsx | 2 + .../components/Node/AddNodeButtonGroup.tsx | 18 ++++++++- src/web/topic/store/nodeTypeHooks.ts | 24 ++++++++++++ src/web/topic/utils/edge.ts | 39 ++++++++++++++++--- src/web/topic/utils/layout.ts | 2 + src/web/topic/utils/node.ts | 9 +++++ 12 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 src/db/migrations/20241216152339_add_mitigation_node/down.sql create mode 100644 src/db/migrations/20241216152339_add_mitigation_node/migration.sql diff --git a/src/common/edge.ts b/src/common/edge.ts index e36048aa..0bdebad4 100644 --- a/src/common/edge.ts +++ b/src/common/edge.ts @@ -16,6 +16,7 @@ export const relationNames = [ "creates", "fulfills", "obstacleOf", + "mitigates", // research "asksAbout", //question to any node @@ -66,6 +67,7 @@ export const infoRelationNames: Record = { "creates", "fulfills", "obstacleOf", + "mitigates", "relatesTo", // is a generic relation but currently only seems worthwhile in topic ], research: ["asksAbout", "potentialAnswerTo", "relevantFor", "sourceOf", "mentions"], diff --git a/src/common/node.ts b/src/common/node.ts index 025be7ff..98e64d92 100644 --- a/src/common/node.ts +++ b/src/common/node.ts @@ -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", @@ -67,6 +73,8 @@ export const infoNodeTypes: Record = { "solutionComponent", "solution", "obstacle", + "mitigationComponent", + "mitigation", "custom", // is a generic node but currently only seems worthwhile in topic ], research: ["question", "answer", "fact", "source"], diff --git a/src/db/migrations/20241216152339_add_mitigation_node/down.sql b/src/db/migrations/20241216152339_add_mitigation_node/down.sql new file mode 100644 index 00000000..585bad04 --- /dev/null +++ b/src/db/migrations/20241216152339_add_mitigation_node/down.sql @@ -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; diff --git a/src/db/migrations/20241216152339_add_mitigation_node/migration.sql b/src/db/migrations/20241216152339_add_mitigation_node/migration.sql new file mode 100644 index 00000000..44ebd09c --- /dev/null +++ b/src/db/migrations/20241216152339_add_mitigation_node/migration.sql @@ -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; diff --git a/src/db/schema.prisma b/src/db/schema.prisma index f6bd2532..5cd3333e 100644 --- a/src/db/schema.prisma +++ b/src/db/schema.prisma @@ -93,6 +93,8 @@ enum NodeType { solutionComponent solution obstacle + mitigationComponent + mitigation // research question @@ -143,6 +145,7 @@ enum EdgeType { creates fulfills obstacleOf + mitigates // research asksAbout diff --git a/src/web/common/theme.ts b/src/web/common/theme.ts index 7d93cb83..0f2758e6 100644 --- a/src/web/common/theme.ts +++ b/src/web/common/theme.ts @@ -26,6 +26,7 @@ * 150 answer * 160 criterion * 200 support + * 230 mitigation, component (desaturated) * 240 source * 300 problem * 320 cause @@ -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 diff --git a/src/web/topic/components/Diagram/Diagram.tsx b/src/web/topic/components/Diagram/Diagram.tsx index b75680f5..fc0f9c27 100644 --- a/src/web/topic/components/Diagram/Diagram.tsx +++ b/src/web/topic/components/Diagram/Diagram.tsx @@ -49,6 +49,8 @@ const nodeTypes: Record> = { benefit: buildNodeComponent("benefit"), detriment: buildNodeComponent("detriment"), obstacle: buildNodeComponent("obstacle"), + mitigation: buildNodeComponent("mitigation"), + mitigationComponent: buildNodeComponent("mitigationComponent"), // research question: buildNodeComponent("question"), diff --git a/src/web/topic/components/Node/AddNodeButtonGroup.tsx b/src/web/topic/components/Node/AddNodeButtonGroup.tsx index 9942d873..7ed45fb2 100644 --- a/src/web/topic/components/Node/AddNodeButtonGroup.tsx +++ b/src/web/topic/components/Node/AddNodeButtonGroup.tsx @@ -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"; @@ -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) @@ -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 <>; diff --git a/src/web/topic/store/nodeTypeHooks.ts b/src/web/topic/store/nodeTypeHooks.ts index a9dcd8fb..187e96e6 100644 --- a/src/web/topic/store/nodeTypeHooks.ts +++ b/src/web/topic/store/nodeTypeHooks.ts @@ -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 { diff --git a/src/web/topic/utils/edge.ts b/src/web/topic/utils/edge.ts index 9b080b27..06a05c57 100644 --- a/src/web/topic/utils/edge.ts +++ b/src/web/topic/utils/edge.ts @@ -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" }, @@ -94,6 +99,8 @@ 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" }, @@ -101,18 +108,40 @@ export const relations: AddableRelation[] = researchRelations.concat([ { 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" }, diff --git a/src/web/topic/utils/layout.ts b/src/web/topic/utils/layout.ts index 7a3ab9df..c3c32249 100644 --- a/src/web/topic/utils/layout.ts +++ b/src/web/topic/utils/layout.ts @@ -63,6 +63,8 @@ const partitionOrders: { [type in NodeType]: string } = { solutionComponent: "2", solution: "3", obstacle: "null", + mitigationComponent: "null", + mitigation: "null", // research question: "null", diff --git a/src/web/topic/utils/node.ts b/src/web/topic/utils/node.ts index 70e69fd2..bdf4bb63 100644 --- a/src/web/topic/utils/node.ts +++ b/src/web/topic/utils/node.ts @@ -15,6 +15,7 @@ import { QuestionMark, ThumbDown, ThumbUp, + VerifiedUserOutlined, Widgets, } from "@mui/icons-material"; @@ -76,6 +77,14 @@ export const nodeDecorations: Record = { title: "Obstacle", NodeIcon: Fence, }, + mitigationComponent: { + title: "Component", + NodeIcon: Widgets, + }, + mitigation: { + title: "Mitigation", + NodeIcon: VerifiedUserOutlined, + }, // research question: { From 87d51587a0646fc81faaaa81846a5bf8afc00efe Mon Sep 17 00:00:00 2001 From: Joel Keyser Date: Mon, 16 Dec 2024 17:41:53 -0600 Subject: [PATCH 3/3] touchup: show below-detriments in solution filter not really sure what's best here, but this is following the logic of showing pieces of the selected solution. currently, mitigations aren't considered a piece of the selected solution, so they aren't shown in the filter. --- src/web/view/utils/infoFilter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/view/utils/infoFilter.ts b/src/web/view/utils/infoFilter.ts index 7e1329bb..fe2455b7 100644 --- a/src/web/view/utils/infoFilter.ts +++ b/src/web/view/utils/infoFilter.ts @@ -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);