-
Notifications
You must be signed in to change notification settings - Fork 7
ENG-2083 Add schema selection panel UI for Obsidian export/import #1262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: eng-1975-schema-foundation
Are you sure you want to change the base?
Changes from all commits
a2367b5
647cdca
ace9557
437c314
07ccd44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ); | ||
| }; | ||
| 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use the native obsidian |
||
| }; | ||
|
|
||
| export const SchemaSelectionPanel = ({ | ||
| source, | ||
| selection, | ||
| onDependencyViolation, | ||
| }: SchemaSelectionPanelProps) => { | ||
| const { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same with these. How many of these need to be props? Or could they just be inline? |
||
| 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> | ||
| </> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How many of these need to be props? Stated differently, how many of these are being reused in multiple component instances? It adds a lot of noise and complexity to put all of these in props if they are one time use.