Skip to content
Open
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
65 changes: 65 additions & 0 deletions apps/obsidian/src/components/SchemaSelectionModalBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { SchemaSelectionPanel } from "~/components/SchemaSelectionPanel";
import type {
SchemaSelectionSource,
SchemaSelectionState,
} from "~/components/useSchemaSelection";

type SchemaSelectionModalBodyProps = {
title: string;
description: string;
source: SchemaSelectionSource;
selection: SchemaSelectionState;
onDependencyViolation?: (message: string) => void;
footerSecondaryLabel: string;
onFooterSecondaryClick: () => void;
footerPrimaryLabel: string;
onFooterPrimaryClick: () => void;
isFooterPrimaryDisabled?: boolean;
isFooterSecondaryDisabled?: boolean;
};

export const SchemaSelectionModalBody = ({
title,
description,
source,
selection,
onDependencyViolation,
footerSecondaryLabel,
onFooterSecondaryClick,
footerPrimaryLabel,
onFooterPrimaryClick,
isFooterPrimaryDisabled = false,
isFooterSecondaryDisabled = false,
}: SchemaSelectionModalBodyProps) => {
return (
<div>
<h3 className="mb-2">{title}</h3>
<p className="text-muted mb-4 text-sm">{description}</p>

<SchemaSelectionPanel
source={source}
selection={selection}
onDependencyViolation={onDependencyViolation}
/>

<div className="mt-6 flex justify-between">
<button
type="button"
className="px-4 py-2"
onClick={onFooterSecondaryClick}
disabled={isFooterSecondaryDisabled}
>
{footerSecondaryLabel}
</button>
<button
type="button"
className="!bg-accent !text-on-accent rounded px-4 py-2"
onClick={onFooterPrimaryClick}
disabled={isFooterPrimaryDisabled}
>
{footerPrimaryLabel}
</button>
</div>
</div>
);
};
296 changes: 296 additions & 0 deletions apps/obsidian/src/components/SchemaSelectionPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import type {
SchemaSelectionSource,
SchemaSelectionState,
} from "~/components/useSchemaSelection";

type SchemaSelectionPanelProps = {
source: SchemaSelectionSource;
selection: SchemaSelectionState;
onDependencyViolation?: (message: string) => void;
};

export const SchemaSelectionPanel = ({
source,
selection,
onDependencyViolation,
}: SchemaSelectionPanelProps) => {
const {
selectedNodeTypeIds,
selectedRelationTypeIds,
selectedRelationIds,
selectedTemplateNames,
requiredNodeTypeIds,
requiredRelationTypeIds,
selectAllNodeTypes,
deselectOptionalNodeTypes,
toggleNodeType,
selectAllRelationTypes,
deselectOptionalRelationTypes,
toggleRelationType,
selectAllRelationTriples,
deselectAllRelationTriples,
toggleRelationTriple,
selectAllTemplates,
deselectAllTemplates,
toggleTemplate,
} = selection;

const nodeTypeById = new Map(
source.nodeTypes.map((nodeType) => [nodeType.id, nodeType]),
);
const relationTypeById = new Map(
source.relationTypes.map((relationType) => [relationType.id, relationType]),
);
const sortedNodeTypes = [...source.nodeTypes].sort((a, b) =>
a.name.localeCompare(b.name),
);
const templateToNodeTypeNames = new Map<string, string[]>();
for (const nodeType of sortedNodeTypes) {
if (!nodeType.template) continue;
const names = templateToNodeTypeNames.get(nodeType.template) ?? [];
names.push(nodeType.name);
templateToNodeTypeNames.set(nodeType.template, names);
}
const referencedTemplateNames = new Set(templateToNodeTypeNames.keys());

return (
<>
<div className="mb-4 rounded border p-3 text-sm">
<div className="font-medium">Selection summary</div>
<div className="text-muted mt-1 flex flex-wrap gap-4">
<span>{selectedNodeTypeIds.size} node type(s)</span>
<span>{selectedRelationTypeIds.size} relation type(s)</span>
<span>{selectedRelationIds.size} relation triple(s)</span>
<span>{selectedTemplateNames.size} template(s)</span>
</div>
</div>

<div className="max-h-96 space-y-4 overflow-y-auto">
<section className="rounded border p-3">
<div className="mb-2 flex items-center justify-between">
<h4 className="font-medium">Node types</h4>
<div className="flex gap-2">
<button
type="button"
className="rounded border px-2 py-1 text-xs"
onClick={selectAllNodeTypes}
>
Select all
</button>
<button
type="button"
className="rounded border px-2 py-1 text-xs"
onClick={deselectOptionalNodeTypes}
>
Deselect optional
</button>
</div>
</div>
<div className="space-y-1">
{source.nodeTypes.map((nodeType) => {
const isRequired = requiredNodeTypeIds.has(nodeType.id);
return (
<label
key={nodeType.id}
className="flex items-center gap-2 text-sm"
>
<input
type="checkbox"
checked={selectedNodeTypeIds.has(nodeType.id)}
onChange={(event) => {
const result = toggleNodeType(
nodeType.id,
event.target.checked,
);
if (
!result.ok &&
result.reason &&
onDependencyViolation
) {
onDependencyViolation(result.reason);
}
}}
disabled={isRequired}
/>
<span>{nodeType.name}</span>
{isRequired && (
<span className="text-muted text-xs">
required by selected triple
</span>
)}
</label>
);
})}
</div>
</section>

<section className="rounded border p-3">
<div className="mb-2 flex items-center justify-between">
<h4 className="font-medium">Relation types</h4>
<div className="flex gap-2">
<button
type="button"
className="rounded border px-2 py-1 text-xs"
onClick={selectAllRelationTypes}
>
Select all
</button>
<button
type="button"
className="rounded border px-2 py-1 text-xs"
onClick={deselectOptionalRelationTypes}
>
Deselect optional
</button>
</div>
</div>
<div className="space-y-1">
{source.relationTypes.map((relationType) => {
const isRequired = requiredRelationTypeIds.has(relationType.id);
return (
<label
key={relationType.id}
className="flex items-center gap-2 text-sm"
>
<input
type="checkbox"
checked={selectedRelationTypeIds.has(relationType.id)}
onChange={(event) => {
const result = toggleRelationType(
relationType.id,
event.target.checked,
);
if (
!result.ok &&
result.reason &&
onDependencyViolation
) {
onDependencyViolation(result.reason);
}
}}
disabled={isRequired}
/>
<span>{relationType.label}</span>
{isRequired && (
<span className="text-muted text-xs">
required by selected triple
</span>
)}
</label>
);
})}
</div>
</section>

<section className="rounded border p-3">
<div className="mb-2 flex items-center justify-between">
<h4 className="font-medium">Relation triples</h4>
<div className="flex gap-2">
<button
type="button"
className="rounded border px-2 py-1 text-xs"
onClick={selectAllRelationTriples}
>
Select all
</button>
<button
type="button"
className="rounded border px-2 py-1 text-xs"
onClick={deselectAllRelationTriples}
>
Deselect all
</button>
</div>
</div>
<div className="space-y-1">
{source.relationTriples.map((relation) => {
const sourceName =
nodeTypeById.get(relation.sourceId)?.name ?? relation.sourceId;
const destinationName =
nodeTypeById.get(relation.destinationId)?.name ??
relation.destinationId;
const relationTypeLabel =
relationTypeById.get(relation.relationshipTypeId)?.label ??
relation.relationshipTypeId;

return (
<label
key={relation.id}
className="flex items-center gap-2 text-sm"
>
<input
type="checkbox"
checked={selectedRelationIds.has(relation.id)}
onChange={(event) =>
toggleRelationTriple(relation.id, event.target.checked)
}
/>
<span className="rounded bg-secondary px-1.5 py-0.5 text-xs">
{sourceName}
</span>
<span className="text-accent text-xs font-medium">
{relationTypeLabel}
</span>
<span className="rounded bg-secondary px-1.5 py-0.5 text-xs">
{destinationName}
</span>
</label>
);
})}
</div>
</section>

<section className="rounded border p-3">
<div className="mb-2 flex items-center justify-between">
<h4 className="font-medium">Templates</h4>
<div className="flex gap-2">
<button
type="button"
className="rounded border px-2 py-1 text-xs"
onClick={selectAllTemplates}
>
Select all
</button>
<button
type="button"
className="rounded border px-2 py-1 text-xs"
onClick={deselectAllTemplates}
>
Deselect all
</button>
</div>
</div>
{source.templateNames.length === 0 ? (
<p className="text-muted text-sm">No template files found.</p>
) : (
<div className="space-y-1">
{source.templateNames.map((templateName) => (
<label
key={templateName}
className="flex items-center gap-2 text-sm"
>
<input
type="checkbox"
checked={selectedTemplateNames.has(templateName)}
onChange={(event) =>
toggleTemplate(templateName, event.target.checked)
}
/>
<span>{templateName}.md</span>
{referencedTemplateNames.has(templateName) && (
<span className="text-muted rounded bg-secondary px-1.5 py-0.5 text-xs">
used by{" "}
{(templateToNodeTypeNames.get(templateName) ?? []).join(
", ",
)}
</span>
)}
</label>
))}
</div>
)}
</section>
</div>
</>
);
};
Loading