Skip to content
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
9 changes: 9 additions & 0 deletions components/decision-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ export function DecisionSummary({
</div>
)}

{decision.decisionNotes && !compact && (
<div className="space-y-2">
<h3 className="text-muted-foreground">Notes</h3>
<div className="prose prose-sm dark:prose-invert max-w-none">
<TipTapView content={decision.decisionNotes} />
</div>
</div>
)}

{!compact && (
<div className="grid grid-cols-2 gap-4 pt-4 border-t">
<div className="space-y-2">
Expand Down
66 changes: 40 additions & 26 deletions components/tiptap-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from "@/lib/utils"

interface TipTapEditorProps {
content: string
onChange: (content: string) => void
className?: string
minimal?: boolean
}

export function TipTapEditor({ content, onChange, className = '' }: TipTapEditorProps) {
const getEditorClassNames = (minimal: boolean) => cn(
'prose prose-sm dark:prose-invert focus:outline-none max-w-none',
minimal ? 'p-2' : 'p-4 min-h-[200px]'
);

export function TipTapEditor({ content, onChange, className = '', minimal = false }: TipTapEditorProps) {
const [isFocused, setIsFocused] = React.useState(false);
const [isRawMode, setIsRawMode] = React.useState(false);
const [rawMarkdown, setRawMarkdown] = React.useState(content || '');
Expand Down Expand Up @@ -53,7 +60,7 @@ export function TipTapEditor({ content, onChange, className = '' }: TipTapEditor
},
editorProps: {
attributes: {
class: 'prose prose-sm dark:prose-invert focus:outline-none max-w-none p-4 min-h-[200px]'
class: getEditorClassNames(minimal)
}
},
onFocus: () => setIsFocused(true),
Expand Down Expand Up @@ -173,35 +180,42 @@ export function TipTapEditor({ content, onChange, className = '' }: TipTapEditor
];

return (
<Card className={`min-h-[300px] ${className}`}>
<div className="flex items-center gap-1 border-b p-2">
<TooltipProvider>
{tools
.filter(tool => isRawMode ? tool.showInRawMode : true)
.map((Tool) => (
<Tooltip key={Tool.title}>
<TooltipTrigger asChild>
<Button
variant={Tool.isActive() ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={Tool.action}
title={Tool.title}
>
<Tool.icon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{Tool.title}</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
</div>
<Card className={cn(
minimal ? 'min-h-0 border-0 shadow-none bg-transparent' : 'min-h-[300px]',
className
)}>
{!minimal && (
<div className="flex items-center gap-1 border-b p-2">
<TooltipProvider>
{tools
.filter(tool => isRawMode ? tool.showInRawMode : true)
.map((Tool) => (
<Tooltip key={Tool.title}>
<TooltipTrigger asChild>
<Button
variant={Tool.isActive() ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={Tool.action}
title={Tool.title}
>
<Tool.icon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{Tool.title}</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
</div>
)}

{isRawMode ? (
<Textarea
value={rawMarkdown}
onChange={handleRawMarkdownChange}
className="min-h-[200px] resize-none border-none rounded-none focus-visible:ring-0 p-4 font-mono text-sm"
className={`resize-none border-none rounded-none focus-visible:ring-0 p-4 font-mono text-sm ${
minimal ? 'min-h-0' : 'min-h-[200px]'
}`}
placeholder="Enter markdown here..."
/>
) : (
Expand Down
36 changes: 27 additions & 9 deletions components/workflow/WorkflowAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export default function WorkflowAccordion({
removeStakeholder,
updateStakeholders,
publishDecision,
updateDecisionNotes,
} = useDecision(decisionId, organisationId)

const {
Expand Down Expand Up @@ -457,25 +458,42 @@ export default function WorkflowAccordion({
if (step.key === 'choose' && decision) {
return (
<div className="space-y-8">
<div className="space-y-4">
<h2 className="text-xl text-muted-foreground">Notes</h2>
<TipTapEditor
content={decision.decisionNotes || ""}
onChange={(content) => updateDecisionNotes(content)}
/>
</div>

<DecisionRelationshipsList
relationshipType="blocked_by"
fromDecision={decision}
title="Blocked By Decision(s)"
/>

<div className="space-y-4">
<h2 className="text-xl text-muted-foreground">Decision</h2>
<TipTapEditor
content={decision.decision || ""}
onChange={(content) => updateDecisionContent(content)}
/>
</div>

<SupportingMaterialsList
<SupportingMaterialsList
materials={decision.supportingMaterials}
onAdd={addSupportingMaterial}
onRemove={removeSupportingMaterial}
/>

<div className="space-y-4">
<div className="flex items-center gap-2">
<h2 className="text-xl text-muted-foreground">Decision</h2>
<span className="text-sm text-muted-foreground">
- state your decision concisely in 1-2 sentences
</span>
</div>
<div className="rounded-md border">
<TipTapEditor
content={decision.decision || ""}
onChange={(content) => updateDecisionContent(content)}
className="prose-sm min-h-[4rem] max-h-[8rem] overflow-y-auto"
minimal
/>
</div>
</div>
</div>
)
}
Expand Down
18 changes: 18 additions & 0 deletions hooks/useDecisions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,23 @@ export function useDecision(decisionId: string, organisationId: string) {
}
};

/**
* Updates the notes associated with a decision
* @param decisionNotes - The new notes content in HTML format
* @throws {Error} If the update fails or if no decision is loaded
*/
const updateDecisionNotes = async (decisionNotes: string) => {
try {
if (!decision) return;
await decisionsRepository.update(
decision.with({ decisionNotes }),
);
} catch (error) {
setError(error as Error);
throw error;
}
};

return {
decision,
loading,
Expand All @@ -243,5 +260,6 @@ export function useDecision(decisionId: string, organisationId: string) {
addSupportingMaterial,
removeSupportingMaterial,
publishDecision,
updateDecisionNotes,
};
}
6 changes: 6 additions & 0 deletions lib/domain/Decision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export type DecisionProps = {
teamIds: string[];
projectIds: string[];
relationships?: DecisionRelationshipMap;
decisionNotes?: string;
};

export class Decision {
Expand Down Expand Up @@ -175,6 +176,10 @@ export class Decision {
@IsOptional()
readonly relationships?: DecisionRelationshipMap;

@IsOptional()
@IsString()
readonly decisionNotes?: string;

toDocumentReference(): DocumentReference {
return {
id: this.id,
Expand Down Expand Up @@ -368,6 +373,7 @@ export class Decision {
this.teamIds = props.teamIds || [];
this.projectIds = props.projectIds || [];
this.relationships = props.relationships;
this.decisionNotes = props.decisionNotes;
this.validate();
}

Expand Down
21 changes: 12 additions & 9 deletions lib/infrastructure/firestoreDecisionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class FirestoreDecisionsRepository implements DecisionsRepository {
teamIds: data.teamIds || [],
projectIds: data.projectIds || [],
relationships: data.relationships || {},
decisionNotes: data.decisionNotes || '',
};
return Decision.create(props);
}
Expand Down Expand Up @@ -94,10 +95,10 @@ export class FirestoreDecisionsRepository implements DecisionsRepository {
return this.decisionFromFirestore(docSnap)
}

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

const createData: Record<string, string | string[] | null | FieldValue | Record<string, unknown> | DecisionStakeholderRole[] | SupportingMaterial[]> = {
const data: Record<string, string | string[] | null | FieldValue | Record<string, unknown> | DecisionStakeholderRole[] | SupportingMaterial[]> = {
title: initialData.title ?? '',
description: initialData.description ?? '',
cost: initialData.cost ?? 'low',
Expand All @@ -110,16 +111,17 @@ export class FirestoreDecisionsRepository implements DecisionsRepository {
organisationId: scope.organisationId,
teamIds: initialData.teamIds ?? [],
projectIds: initialData.projectIds ?? [],
decisionNotes: initialData.decisionNotes ?? '',
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
}

// Filter out any undefined values
const filteredCreateData = Object.fromEntries(
Object.entries(createData).filter(([, value]) => value !== undefined)
const filteredData = Object.fromEntries(
Object.entries(data).filter(([, value]) => value !== undefined)
);

await setDoc(docRef, filteredCreateData)
await setDoc(docRef, filteredData)

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

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

const updateData: Record<string, FieldValue | Partial<unknown> | undefined> = {
const data: Record<string, FieldValue | Partial<unknown> | undefined> = {
title: decision.title,
description: decision.description,
cost: decision.cost,
Expand All @@ -145,15 +147,16 @@ export class FirestoreDecisionsRepository implements DecisionsRepository {
teamIds: decision.teamIds,
projectIds: decision.projectIds,
publishDate: decision.publishDate,
decisionNotes: decision.decisionNotes,
updatedAt: serverTimestamp()
}

// Filter out any undefined values
const filteredUpdateData = Object.fromEntries(
Object.entries(updateData).filter(([, value]) => value !== undefined)
const filteredData = Object.fromEntries(
Object.entries(data).filter(([, value]) => value !== undefined)
);

await updateDoc(docRef, filteredUpdateData)
await updateDoc(docRef, filteredData)
}

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