Skip to content

Commit fc3731d

Browse files
authored
Merge pull request #26 from wellmaintained/feature/decision-notes
feature: Decision notes
2 parents 65b5d11 + 76ba238 commit fc3731d

File tree

6 files changed

+112
-44
lines changed

6 files changed

+112
-44
lines changed

components/decision-summary.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ export function DecisionSummary({
9292
</div>
9393
)}
9494

95+
{decision.decisionNotes && !compact && (
96+
<div className="space-y-2">
97+
<h3 className="text-muted-foreground">Notes</h3>
98+
<div className="prose prose-sm dark:prose-invert max-w-none">
99+
<TipTapView content={decision.decisionNotes} />
100+
</div>
101+
</div>
102+
)}
103+
95104
{!compact && (
96105
<div className="grid grid-cols-2 gap-4 pt-4 border-t">
97106
<div className="space-y-2">

components/tiptap-editor.tsx

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@ import {
1414
TooltipProvider,
1515
TooltipTrigger,
1616
} from '@/components/ui/tooltip'
17+
import { cn } from "@/lib/utils"
1718

1819
interface TipTapEditorProps {
1920
content: string
2021
onChange: (content: string) => void
2122
className?: string
23+
minimal?: boolean
2224
}
2325

24-
export function TipTapEditor({ content, onChange, className = '' }: TipTapEditorProps) {
26+
const getEditorClassNames = (minimal: boolean) => cn(
27+
'prose prose-sm dark:prose-invert focus:outline-none max-w-none',
28+
minimal ? 'p-2' : 'p-4 min-h-[200px]'
29+
);
30+
31+
export function TipTapEditor({ content, onChange, className = '', minimal = false }: TipTapEditorProps) {
2532
const [isFocused, setIsFocused] = React.useState(false);
2633
const [isRawMode, setIsRawMode] = React.useState(false);
2734
const [rawMarkdown, setRawMarkdown] = React.useState(content || '');
@@ -53,7 +60,7 @@ export function TipTapEditor({ content, onChange, className = '' }: TipTapEditor
5360
},
5461
editorProps: {
5562
attributes: {
56-
class: 'prose prose-sm dark:prose-invert focus:outline-none max-w-none p-4 min-h-[200px]'
63+
class: getEditorClassNames(minimal)
5764
}
5865
},
5966
onFocus: () => setIsFocused(true),
@@ -173,35 +180,42 @@ export function TipTapEditor({ content, onChange, className = '' }: TipTapEditor
173180
];
174181

175182
return (
176-
<Card className={`min-h-[300px] ${className}`}>
177-
<div className="flex items-center gap-1 border-b p-2">
178-
<TooltipProvider>
179-
{tools
180-
.filter(tool => isRawMode ? tool.showInRawMode : true)
181-
.map((Tool) => (
182-
<Tooltip key={Tool.title}>
183-
<TooltipTrigger asChild>
184-
<Button
185-
variant={Tool.isActive() ? "secondary" : "ghost"}
186-
size="icon"
187-
className="h-8 w-8"
188-
onClick={Tool.action}
189-
title={Tool.title}
190-
>
191-
<Tool.icon className="h-4 w-4" />
192-
</Button>
193-
</TooltipTrigger>
194-
<TooltipContent>{Tool.title}</TooltipContent>
195-
</Tooltip>
196-
))}
197-
</TooltipProvider>
198-
</div>
183+
<Card className={cn(
184+
minimal ? 'min-h-0 border-0 shadow-none bg-transparent' : 'min-h-[300px]',
185+
className
186+
)}>
187+
{!minimal && (
188+
<div className="flex items-center gap-1 border-b p-2">
189+
<TooltipProvider>
190+
{tools
191+
.filter(tool => isRawMode ? tool.showInRawMode : true)
192+
.map((Tool) => (
193+
<Tooltip key={Tool.title}>
194+
<TooltipTrigger asChild>
195+
<Button
196+
variant={Tool.isActive() ? "secondary" : "ghost"}
197+
size="icon"
198+
className="h-8 w-8"
199+
onClick={Tool.action}
200+
title={Tool.title}
201+
>
202+
<Tool.icon className="h-4 w-4" />
203+
</Button>
204+
</TooltipTrigger>
205+
<TooltipContent>{Tool.title}</TooltipContent>
206+
</Tooltip>
207+
))}
208+
</TooltipProvider>
209+
</div>
210+
)}
199211

200212
{isRawMode ? (
201213
<Textarea
202214
value={rawMarkdown}
203215
onChange={handleRawMarkdownChange}
204-
className="min-h-[200px] resize-none border-none rounded-none focus-visible:ring-0 p-4 font-mono text-sm"
216+
className={`resize-none border-none rounded-none focus-visible:ring-0 p-4 font-mono text-sm ${
217+
minimal ? 'min-h-0' : 'min-h-[200px]'
218+
}`}
205219
placeholder="Enter markdown here..."
206220
/>
207221
) : (

components/workflow/WorkflowAccordion.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export default function WorkflowAccordion({
9090
removeStakeholder,
9191
updateStakeholders,
9292
publishDecision,
93+
updateDecisionNotes,
9394
} = useDecision(decisionId, organisationId)
9495

9596
const {
@@ -457,25 +458,42 @@ export default function WorkflowAccordion({
457458
if (step.key === 'choose' && decision) {
458459
return (
459460
<div className="space-y-8">
461+
<div className="space-y-4">
462+
<h2 className="text-xl text-muted-foreground">Notes</h2>
463+
<TipTapEditor
464+
content={decision.decisionNotes || ""}
465+
onChange={(content) => updateDecisionNotes(content)}
466+
/>
467+
</div>
468+
460469
<DecisionRelationshipsList
461470
relationshipType="blocked_by"
462471
fromDecision={decision}
463472
title="Blocked By Decision(s)"
464473
/>
465474

466-
<div className="space-y-4">
467-
<h2 className="text-xl text-muted-foreground">Decision</h2>
468-
<TipTapEditor
469-
content={decision.decision || ""}
470-
onChange={(content) => updateDecisionContent(content)}
471-
/>
472-
</div>
473-
474-
<SupportingMaterialsList
475+
<SupportingMaterialsList
475476
materials={decision.supportingMaterials}
476477
onAdd={addSupportingMaterial}
477478
onRemove={removeSupportingMaterial}
478479
/>
480+
481+
<div className="space-y-4">
482+
<div className="flex items-center gap-2">
483+
<h2 className="text-xl text-muted-foreground">Decision</h2>
484+
<span className="text-sm text-muted-foreground">
485+
- state your decision concisely in 1-2 sentences
486+
</span>
487+
</div>
488+
<div className="rounded-md border">
489+
<TipTapEditor
490+
content={decision.decision || ""}
491+
onChange={(content) => updateDecisionContent(content)}
492+
className="prose-sm min-h-[4rem] max-h-[8rem] overflow-y-auto"
493+
minimal
494+
/>
495+
</div>
496+
</div>
479497
</div>
480498
)
481499
}

hooks/useDecisions.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,23 @@ export function useDecision(decisionId: string, organisationId: string) {
225225
}
226226
};
227227

228+
/**
229+
* Updates the notes associated with a decision
230+
* @param decisionNotes - The new notes content in HTML format
231+
* @throws {Error} If the update fails or if no decision is loaded
232+
*/
233+
const updateDecisionNotes = async (decisionNotes: string) => {
234+
try {
235+
if (!decision) return;
236+
await decisionsRepository.update(
237+
decision.with({ decisionNotes }),
238+
);
239+
} catch (error) {
240+
setError(error as Error);
241+
throw error;
242+
}
243+
};
244+
228245
return {
229246
decision,
230247
loading,
@@ -243,5 +260,6 @@ export function useDecision(decisionId: string, organisationId: string) {
243260
addSupportingMaterial,
244261
removeSupportingMaterial,
245262
publishDecision,
263+
updateDecisionNotes,
246264
};
247265
}

lib/domain/Decision.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export type DecisionProps = {
115115
teamIds: string[];
116116
projectIds: string[];
117117
relationships?: DecisionRelationshipMap;
118+
decisionNotes?: string;
118119
};
119120

120121
export class Decision {
@@ -175,6 +176,10 @@ export class Decision {
175176
@IsOptional()
176177
readonly relationships?: DecisionRelationshipMap;
177178

179+
@IsOptional()
180+
@IsString()
181+
readonly decisionNotes?: string;
182+
178183
toDocumentReference(): DocumentReference {
179184
return {
180185
id: this.id,
@@ -368,6 +373,7 @@ export class Decision {
368373
this.teamIds = props.teamIds || [];
369374
this.projectIds = props.projectIds || [];
370375
this.relationships = props.relationships;
376+
this.decisionNotes = props.decisionNotes;
371377
this.validate();
372378
}
373379

lib/infrastructure/firestoreDecisionsRepository.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class FirestoreDecisionsRepository implements DecisionsRepository {
5555
teamIds: data.teamIds || [],
5656
projectIds: data.projectIds || [],
5757
relationships: data.relationships || {},
58+
decisionNotes: data.decisionNotes || '',
5859
};
5960
return Decision.create(props);
6061
}
@@ -94,10 +95,10 @@ export class FirestoreDecisionsRepository implements DecisionsRepository {
9495
return this.decisionFromFirestore(docSnap)
9596
}
9697

97-
async create(initialData: Partial<Omit<DecisionProps, "id">>, scope: DecisionScope): Promise<Decision> {
98+
async create(scope: DecisionScope, initialData: Partial<DecisionProps> = {}): Promise<Decision> {
9899
const docRef = doc(collection(db, this.getDecisionPath(scope)))
99100

100-
const createData: Record<string, string | string[] | null | FieldValue | Record<string, unknown> | DecisionStakeholderRole[] | SupportingMaterial[]> = {
101+
const data: Record<string, string | string[] | null | FieldValue | Record<string, unknown> | DecisionStakeholderRole[] | SupportingMaterial[]> = {
101102
title: initialData.title ?? '',
102103
description: initialData.description ?? '',
103104
cost: initialData.cost ?? 'low',
@@ -110,16 +111,17 @@ export class FirestoreDecisionsRepository implements DecisionsRepository {
110111
organisationId: scope.organisationId,
111112
teamIds: initialData.teamIds ?? [],
112113
projectIds: initialData.projectIds ?? [],
114+
decisionNotes: initialData.decisionNotes ?? '',
113115
createdAt: serverTimestamp(),
114116
updatedAt: serverTimestamp(),
115117
}
116118

117119
// Filter out any undefined values
118-
const filteredCreateData = Object.fromEntries(
119-
Object.entries(createData).filter(([, value]) => value !== undefined)
120+
const filteredData = Object.fromEntries(
121+
Object.entries(data).filter(([, value]) => value !== undefined)
120122
);
121123

122-
await setDoc(docRef, filteredCreateData)
124+
await setDoc(docRef, filteredData)
123125

124126
return this.getById(docRef.id, scope)
125127
}
@@ -131,7 +133,7 @@ export class FirestoreDecisionsRepository implements DecisionsRepository {
131133

132134
const docRef = doc(db, this.getDecisionPath(scope), decision.id)
133135

134-
const updateData: Record<string, FieldValue | Partial<unknown> | undefined> = {
136+
const data: Record<string, FieldValue | Partial<unknown> | undefined> = {
135137
title: decision.title,
136138
description: decision.description,
137139
cost: decision.cost,
@@ -145,15 +147,16 @@ export class FirestoreDecisionsRepository implements DecisionsRepository {
145147
teamIds: decision.teamIds,
146148
projectIds: decision.projectIds,
147149
publishDate: decision.publishDate,
150+
decisionNotes: decision.decisionNotes,
148151
updatedAt: serverTimestamp()
149152
}
150153

151154
// Filter out any undefined values
152-
const filteredUpdateData = Object.fromEntries(
153-
Object.entries(updateData).filter(([, value]) => value !== undefined)
155+
const filteredData = Object.fromEntries(
156+
Object.entries(data).filter(([, value]) => value !== undefined)
154157
);
155158

156-
await updateDoc(docRef, filteredUpdateData)
159+
await updateDoc(docRef, filteredData)
157160
}
158161

159162
async delete(id: string, scope: DecisionScope): Promise<void> {

0 commit comments

Comments
 (0)