@shortlink-org/portolan 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/cli/init.mjs +14 -6
- package/cli/init.test.mjs +8 -1
- package/cli/portolan.mjs +92 -13
- package/cli/portolan.test.mjs +47 -0
- package/package.json +7 -6
- package/plugins/extract-django/README.md +1 -1
- package/plugins/extract-django/extract_test.py +1 -1
- package/plugins/extract-django/openapi.py +4 -14
- package/plugins/extract-django/openapi_test.py +4 -4
- package/plugins/extract-go/options.schema.json +5 -0
- package/plugins/extract-project/options.schema.json +21 -0
- package/plugins/extract-ts/README.md +1 -1
- package/plugins/extract-ts/clients.ts +1 -1
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/graphql.test.ts +2 -2
- package/plugins/extract-ts/openapi.test.ts +6 -7
- package/plugins/extract-ts/openapi.ts +3 -10
- package/plugins/portolan-go.wasm +0 -0
- package/public/404-cat-v1.webp +0 -0
- package/public/cat-about-v1.webp +0 -0
- package/public/cat-clear-skies-v1.webp +0 -0
- package/public/cat-no-changes-v1.webp +0 -0
- package/public/cat-onboarding-v1.webp +0 -0
- package/public/cat-trial-success-v1.webp +0 -0
- package/schema/portolan.schema.json +61 -0
- package/scripts/builtin-plugins.mjs +3 -2
- package/scripts/catalog-sources.mjs +14 -2
- package/scripts/catalog-sources.test.mjs +45 -0
- package/scripts/delivery-presets.mjs +207 -24
- package/scripts/diff.mjs +5 -1
- package/scripts/gen-likec4.mjs +242 -85
- package/scripts/gen-likec4.test.mjs +168 -14
- package/scripts/gen.mjs +5 -3
- package/scripts/host-plugins/fetch-git.mjs +22 -12
- package/scripts/host-plugins/fetch-git.test.mjs +25 -5
- package/scripts/local-api.mjs +330 -346
- package/scripts/local-api.test.mjs +245 -1
- package/scripts/local-discovery.mjs +378 -0
- package/scripts/manifest.mjs +42 -1
- package/scripts/manifest.test.mjs +24 -1
- package/scripts/plugin-host.mjs +14 -3
- package/scripts/plugin-host.test.mjs +8 -0
- package/scripts/run-builtin.mjs +4 -2
- package/scripts/schema.mjs +11 -0
- package/scripts/site-docs.mjs +2 -2
- package/src/app/CatalogApp.tsx +23 -8
- package/src/app/CommandPalette.tsx +7 -2
- package/src/app/Sidebar.tsx +13 -730
- package/src/app/SidebarFlowSections.tsx +251 -0
- package/src/app/SidebarFooter.tsx +160 -0
- package/src/app/SidebarTree.tsx +322 -0
- package/src/app/toast.tsx +11 -7
- package/src/catalog-index.ts +485 -0
- package/src/catalog-model.ts +1339 -0
- package/src/catalog-validation.ts +1570 -0
- package/src/catalog.test.ts +49 -0
- package/src/catalog.ts +6 -3124
- package/src/components/CatIllustration.tsx +30 -0
- package/src/components/CommitLink.tsx +154 -0
- package/src/components/EditorLink.tsx +15 -8
- package/src/components/GrpcMethodReference.tsx +93 -0
- package/src/components/Markdown.tsx +17 -0
- package/src/components/MethodRows.tsx +8 -1
- package/src/components/PageHeader.test.tsx +23 -0
- package/src/components/PageHeader.tsx +34 -0
- package/src/components/ProblemRow.tsx +3 -0
- package/src/components/ShapeRows.tsx +12 -0
- package/src/components/SourceDoc.tsx +27 -12
- package/src/components/SourcePreview.tsx +125 -37
- package/src/enrich.test.ts +209 -2
- package/src/enrich.ts +284 -15
- package/src/er/RedisSchema.tsx +86 -5
- package/src/flow/FlowTable.tsx +19 -4
- package/src/flow/StepDetail.tsx +461 -101
- package/src/flow/StepRail.tsx +36 -13
- package/src/flow/answers-response.test.ts +100 -0
- package/src/flow/answers.ts +11 -8
- package/src/flow/chapters.ts +2 -1
- package/src/flow/labels.ts +10 -0
- package/src/flow/mermaid.test.ts +87 -3
- package/src/flow/mermaid.ts +26 -14
- package/src/index.css +132 -17
- package/src/landing/LandingPage.tsx +16 -12
- package/src/lib/all-problems.ts +1 -1
- package/src/lib/api.test.ts +4 -6
- package/src/lib/build-info.test.ts +29 -0
- package/src/lib/build-info.ts +21 -0
- package/src/lib/data-model.ts +14 -0
- package/src/lib/derive.ts +1 -0
- package/src/lib/flow-tree.ts +1 -1
- package/src/lib/github-catalog.test.ts +51 -0
- package/src/lib/github-catalog.ts +69 -0
- package/src/lib/grpc-reference.test.ts +72 -0
- package/src/lib/grpc-reference.ts +117 -0
- package/src/lib/local-api.ts +36 -11
- package/src/lib/motion.test.ts +4 -2
- package/src/lib/motion.tsx +5 -4
- package/src/lib/product.ts +7 -0
- package/src/lib/proto-problems.test.ts +170 -3
- package/src/lib/proto-problems.ts +176 -4
- package/src/lib/queries.ts +16 -1
- package/src/lib/readme-assets.test.ts +19 -0
- package/src/lib/readme-assets.ts +34 -0
- package/src/lib/setup-info.test.ts +7 -0
- package/src/lib/setup-info.ts +5 -0
- package/src/lib/source-link.test.ts +106 -18
- package/src/lib/source-link.ts +22 -6
- package/src/merge.test.ts +101 -0
- package/src/merge.ts +65 -1
- package/src/pages/AdrDetail.tsx +3 -2
- package/src/pages/AdrIndex.tsx +10 -7
- package/src/pages/AggregatePage.tsx +34 -4
- package/src/pages/BlockPage.tsx +39 -32
- package/src/pages/Changes.tsx +7 -9
- package/src/pages/ContextMap.tsx +11 -2
- package/src/pages/ContextPage.tsx +11 -4
- package/src/pages/FlowIndex.tsx +16 -7
- package/src/pages/GraphPage.tsx +21 -7
- package/src/pages/Language.tsx +9 -9
- package/src/pages/NotFound.tsx +58 -15
- package/src/pages/Overview.tsx +6 -0
- package/src/pages/Problems.tsx +10 -8
- package/src/pages/RegistryIndex.tsx +10 -8
- package/src/pages/ServicePage.tsx +1 -1
- package/src/pages/Settings.tsx +257 -44
- package/src/pages/settings/AboutSettings.tsx +129 -0
- package/src/pages/settings/DeliverySettings.tsx +71 -15
- package/src/routes.test.ts +2 -0
- package/src/routes.ts +2 -1
- package/src/selection/DetailPanel.tsx +213 -113
- package/src/selection/pages.test.ts +14 -1
- package/src/selection/pages.ts +9 -3
- package/src/testing/setup.ts +14 -0
- package/vite.config.ts +3 -1
|
@@ -19,6 +19,7 @@ import { useNarrow } from "../app/responsive";
|
|
|
19
19
|
import { useUiStore } from "../app/ui-store";
|
|
20
20
|
import { catalog, index } from "../data";
|
|
21
21
|
import {
|
|
22
|
+
allRepos,
|
|
22
23
|
blockFields,
|
|
23
24
|
columnId,
|
|
24
25
|
columnNameOfId,
|
|
@@ -29,9 +30,11 @@ import {
|
|
|
29
30
|
viewReads,
|
|
30
31
|
} from "../catalog";
|
|
31
32
|
import { flowsForService, usesOfDef } from "../lib/derive";
|
|
33
|
+
import { stepLabel } from "../flow/labels";
|
|
32
34
|
import { KIND_LABEL } from "../lib/kinds";
|
|
33
35
|
import { eventScope, resolveShape } from "../lib/shape";
|
|
34
36
|
import { stepsInto } from "../lib/backlinks";
|
|
37
|
+
import { treeHref } from "../lib/source-link";
|
|
35
38
|
import { walkSteps } from "../catalog";
|
|
36
39
|
import { methodCount } from "../lib/api";
|
|
37
40
|
import {
|
|
@@ -78,6 +81,26 @@ function Row({ children }: { children: ReactNode }) {
|
|
|
78
81
|
return <div className="flex items-center gap-2 py-0.5">{children}</div>;
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
function PanelSection({
|
|
85
|
+
title,
|
|
86
|
+
meta,
|
|
87
|
+
children,
|
|
88
|
+
}: {
|
|
89
|
+
title: string;
|
|
90
|
+
meta?: ReactNode;
|
|
91
|
+
children: ReactNode;
|
|
92
|
+
}) {
|
|
93
|
+
return (
|
|
94
|
+
<section className="border-b px-3 py-3 last:border-b-0 border-line">
|
|
95
|
+
<div className="mb-2 flex items-baseline justify-between gap-3">
|
|
96
|
+
<h3 className="label">{title}</h3>
|
|
97
|
+
{meta ? <span className="mono text-muted">{meta}</span> : null}
|
|
98
|
+
</div>
|
|
99
|
+
{children}
|
|
100
|
+
</section>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
81
104
|
/** The panel's own way of moving the selection, without leaving the page. */
|
|
82
105
|
function SelectLink({ id, children }: { id: string; children: ReactNode }) {
|
|
83
106
|
const select = useSelectionStore((s) => s.select);
|
|
@@ -699,6 +722,8 @@ function titleOf(resolved: Resolved | null, selection: Selection): string {
|
|
|
699
722
|
switch (resolved.kind) {
|
|
700
723
|
case "event":
|
|
701
724
|
return resolved.event.name;
|
|
725
|
+
case "service":
|
|
726
|
+
return resolved.service.name;
|
|
702
727
|
case "table":
|
|
703
728
|
return resolved.table.name;
|
|
704
729
|
case "view":
|
|
@@ -706,7 +731,7 @@ function titleOf(resolved: Resolved | null, selection: Selection): string {
|
|
|
706
731
|
case "column":
|
|
707
732
|
return `${resolved.view?.name ?? resolved.table?.name ?? resolved.store.slug}.${resolved.column.name}`;
|
|
708
733
|
case "flow-step":
|
|
709
|
-
return resolved.step
|
|
734
|
+
return stepLabel(resolved.step);
|
|
710
735
|
case "bundle":
|
|
711
736
|
return `${resolved.bundle.from} → ${resolved.bundle.to}`;
|
|
712
737
|
// `acme/shop`, not the registry host as well: the host is the same for
|
|
@@ -842,99 +867,155 @@ function ServiceBody({
|
|
|
842
867
|
const methods = methodCount(service);
|
|
843
868
|
const events = service.aggregates.flatMap((a) => a.events);
|
|
844
869
|
const unresolved = service.consumes.filter((c) => c.status === "unresolved");
|
|
870
|
+
const tree = treeHref(service.path, service, allRepos(catalog));
|
|
845
871
|
// Each flow this service takes part in, opened on the first step it is
|
|
846
872
|
// on either end of - where it enters the story, not the top of the rail.
|
|
847
873
|
const appearances = flowsForService(catalog, service.id).map((flow) => {
|
|
848
874
|
const steps = walkSteps(flow.steps);
|
|
849
|
-
const at = steps.findIndex(
|
|
875
|
+
const at = steps.findIndex(
|
|
876
|
+
(s) => s.from === service.id || s.to === service.id,
|
|
877
|
+
);
|
|
850
878
|
return { flow, step: at >= 0 ? steps[at] : undefined, number: at + 1 };
|
|
851
879
|
});
|
|
852
880
|
|
|
853
881
|
return (
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
882
|
+
<section
|
|
883
|
+
aria-label="Service details"
|
|
884
|
+
className="overflow-hidden rounded-card border shadow-xs border-line"
|
|
885
|
+
>
|
|
886
|
+
<PanelSection title="Source">
|
|
887
|
+
<dl className="grid grid-cols-[6.5rem_minmax(0,1fr)] gap-x-4 gap-y-2">
|
|
888
|
+
<dt className="mono text-muted">Repository</dt>
|
|
889
|
+
<dd className="min-w-0">
|
|
890
|
+
<Ident block value={service.repo} className="text-ink" />
|
|
891
|
+
</dd>
|
|
892
|
+
<dt className="mono text-muted">Path</dt>
|
|
893
|
+
<dd className="min-w-0">
|
|
894
|
+
<Ident block value={service.path} className="text-ink" />
|
|
895
|
+
</dd>
|
|
896
|
+
</dl>
|
|
897
|
+
{tree ? (
|
|
898
|
+
<a
|
|
899
|
+
href={tree}
|
|
900
|
+
target="_blank"
|
|
901
|
+
rel="noreferrer"
|
|
902
|
+
className="mono mt-2 inline-flex items-center rounded-control border px-2 py-1 border-line bg-canvas text-accent hover:bg-raised"
|
|
903
|
+
title="Open the service directory on the forge, at the built commit"
|
|
904
|
+
>
|
|
905
|
+
forge ↗
|
|
906
|
+
</a>
|
|
907
|
+
) : null}
|
|
908
|
+
</PanelSection>
|
|
875
909
|
|
|
876
|
-
<
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
<Row key={call.id}>
|
|
882
|
-
<Ident value={call.id} />
|
|
883
|
-
<span className="ml-auto shrink-0">
|
|
884
|
-
<StatusChip status={call.status} title={call.note} />
|
|
885
|
-
</span>
|
|
886
|
-
</Row>
|
|
887
|
-
))}
|
|
888
|
-
{unresolved.length > 0 ? (
|
|
910
|
+
<PanelSection
|
|
911
|
+
title="Provides"
|
|
912
|
+
meta={`${methods} method${methods === 1 ? "" : "s"}`}
|
|
913
|
+
>
|
|
914
|
+
{/* The count opens the tab that lists the interfaces it counted. */}
|
|
889
915
|
<Link
|
|
890
|
-
to={paths.
|
|
891
|
-
className="mono
|
|
916
|
+
to={`${paths.service(context.id, service.slug)}?tab=provides`}
|
|
917
|
+
className="mono rounded-control text-muted hover:text-ink hover:underline"
|
|
892
918
|
>
|
|
893
|
-
|
|
894
|
-
{
|
|
895
|
-
→
|
|
919
|
+
{service.provides.length} interface
|
|
920
|
+
{service.provides.length === 1 ? "" : "s"} →
|
|
896
921
|
</Link>
|
|
897
|
-
|
|
922
|
+
{service.provides.length > 0 ? (
|
|
923
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
924
|
+
{service.provides.map((provided) => (
|
|
925
|
+
<Ident
|
|
926
|
+
block
|
|
927
|
+
key={provided.id}
|
|
928
|
+
value={provided.id}
|
|
929
|
+
className="text-ink"
|
|
930
|
+
/>
|
|
931
|
+
))}
|
|
932
|
+
</div>
|
|
933
|
+
) : null}
|
|
934
|
+
</PanelSection>
|
|
898
935
|
|
|
899
|
-
<
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
936
|
+
<PanelSection
|
|
937
|
+
title="Consumes"
|
|
938
|
+
meta={`${service.consumes.length} call${service.consumes.length === 1 ? "" : "s"}`}
|
|
939
|
+
>
|
|
940
|
+
{service.consumes.length === 0 ? (
|
|
941
|
+
<div className="mono text-muted">this service calls nobody</div>
|
|
942
|
+
) : (
|
|
943
|
+
<div className="flex flex-col gap-1.5">
|
|
944
|
+
{service.consumes.map((call) => (
|
|
945
|
+
<div key={call.id} className="flex flex-wrap items-center gap-2">
|
|
946
|
+
<Ident value={call.id} />
|
|
947
|
+
<StatusChip status={call.status} title={call.note} />
|
|
948
|
+
</div>
|
|
949
|
+
))}
|
|
950
|
+
</div>
|
|
951
|
+
)}
|
|
952
|
+
{unresolved.length > 0 ? (
|
|
906
953
|
<Link
|
|
907
|
-
to={
|
|
908
|
-
|
|
909
|
-
className="mono tnum ml-auto shrink-0 rounded-control text-muted hover:text-ink"
|
|
954
|
+
to={paths.problems()}
|
|
955
|
+
className="mono mt-2 block rounded-control text-unresolved hover:underline"
|
|
910
956
|
>
|
|
911
|
-
{
|
|
957
|
+
<span className="tnum">{unresolved.length}</span> call
|
|
958
|
+
{unresolved.length === 1 ? "" : "s"} resolve to nothing in the
|
|
959
|
+
catalog →
|
|
912
960
|
</Link>
|
|
913
|
-
|
|
914
|
-
|
|
961
|
+
) : null}
|
|
962
|
+
</PanelSection>
|
|
915
963
|
|
|
916
|
-
<
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
className="
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
964
|
+
<PanelSection
|
|
965
|
+
title="Publishes"
|
|
966
|
+
meta={`${events.length} event${events.length === 1 ? "" : "s"}`}
|
|
967
|
+
>
|
|
968
|
+
{events.length === 0 ? (
|
|
969
|
+
<div className="mono text-muted">no events</div>
|
|
970
|
+
) : (
|
|
971
|
+
<div className="flex flex-col gap-1.5">
|
|
972
|
+
{events.map((event) => (
|
|
973
|
+
<div key={event.id} className="flex flex-wrap items-center gap-2">
|
|
974
|
+
<SelectLink id={event.id}>{event.name}</SelectLink>
|
|
975
|
+
<Link
|
|
976
|
+
to={`${eventPathOf(event.id) ?? paths.service(context.id, service.slug)}#${EVENT_ANCHOR.consumers}`}
|
|
977
|
+
title={`${event.consumers.length} consumers of ${event.name}`}
|
|
978
|
+
className="mono tnum shrink-0 rounded-control text-muted hover:text-ink"
|
|
979
|
+
>
|
|
980
|
+
{event.consumers.length} consumers
|
|
981
|
+
</Link>
|
|
982
|
+
</div>
|
|
983
|
+
))}
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
986
|
+
</PanelSection>
|
|
934
987
|
|
|
935
|
-
<
|
|
936
|
-
|
|
937
|
-
|
|
988
|
+
<PanelSection title="Appears in flows" meta={appearances.length}>
|
|
989
|
+
{appearances.length === 0 ? (
|
|
990
|
+
<div className="mono text-muted">appears in no flow</div>
|
|
991
|
+
) : (
|
|
992
|
+
<div className="flex flex-col gap-1.5">
|
|
993
|
+
{appearances.map(({ flow, step, number }) => (
|
|
994
|
+
<Link
|
|
995
|
+
key={flow.slug}
|
|
996
|
+
to={
|
|
997
|
+
step
|
|
998
|
+
? paths.flowStep(flow.slug, step.id)
|
|
999
|
+
: paths.flow(flow.slug)
|
|
1000
|
+
}
|
|
1001
|
+
className="mono flex min-w-0 flex-wrap items-baseline gap-x-2 text-accent"
|
|
1002
|
+
title={step ? `${flow.name}, from step ${number}` : flow.name}
|
|
1003
|
+
>
|
|
1004
|
+
<span className="min-w-0 truncate">{flow.slug}</span>
|
|
1005
|
+
{step ? (
|
|
1006
|
+
<span className="shrink-0 text-muted">step {number}</span>
|
|
1007
|
+
) : null}
|
|
1008
|
+
<span aria-hidden>→</span>
|
|
1009
|
+
</Link>
|
|
1010
|
+
))}
|
|
1011
|
+
</div>
|
|
1012
|
+
)}
|
|
1013
|
+
</PanelSection>
|
|
1014
|
+
|
|
1015
|
+
<PanelSection title="Context">
|
|
1016
|
+
<SelectLink id={context.id}>{context.id}</SelectLink>
|
|
1017
|
+
</PanelSection>
|
|
1018
|
+
</section>
|
|
938
1019
|
);
|
|
939
1020
|
}
|
|
940
1021
|
|
|
@@ -1066,16 +1147,7 @@ function FlowStepBody({
|
|
|
1066
1147
|
}: {
|
|
1067
1148
|
resolved: Extract<Resolved, { kind: "flow-step" }>;
|
|
1068
1149
|
}) {
|
|
1069
|
-
return
|
|
1070
|
-
<>
|
|
1071
|
-
<div className="mono mt-1 mb-2 flex items-center gap-2 text-muted">
|
|
1072
|
-
<Link to={paths.flow(resolved.flow.slug)} className="text-accent">
|
|
1073
|
-
{resolved.flow.slug}
|
|
1074
|
-
</Link>
|
|
1075
|
-
</div>
|
|
1076
|
-
<StepDetailBody step={resolved.step} flow={resolved.flow} />
|
|
1077
|
-
</>
|
|
1078
|
-
);
|
|
1150
|
+
return <StepDetailBody step={resolved.step} flow={resolved.flow} />;
|
|
1079
1151
|
}
|
|
1080
1152
|
|
|
1081
1153
|
/**
|
|
@@ -1208,6 +1280,42 @@ function pinFor(
|
|
|
1208
1280
|
}
|
|
1209
1281
|
}
|
|
1210
1282
|
|
|
1283
|
+
function DetailNavigation({
|
|
1284
|
+
resolved,
|
|
1285
|
+
page,
|
|
1286
|
+
}: {
|
|
1287
|
+
resolved: Resolved | null;
|
|
1288
|
+
page: string | null;
|
|
1289
|
+
}) {
|
|
1290
|
+
if (!page) return null;
|
|
1291
|
+
|
|
1292
|
+
const flowStep = resolved?.kind === "flow-step" ? resolved : null;
|
|
1293
|
+
const destination =
|
|
1294
|
+
flowStep?.flow.slug ??
|
|
1295
|
+
(resolved?.kind === "service" ? "service page" : "catalog page");
|
|
1296
|
+
|
|
1297
|
+
return (
|
|
1298
|
+
<nav
|
|
1299
|
+
aria-label="Selection navigation"
|
|
1300
|
+
className="my-3 flex min-w-0 items-center gap-2 rounded-control border px-2.5 py-2 border-line bg-surface"
|
|
1301
|
+
>
|
|
1302
|
+
<span className="label shrink-0">{flowStep ? "Flow" : "Page"}</span>
|
|
1303
|
+
<span className="mono trunc text-muted" title={destination}>
|
|
1304
|
+
{destination}
|
|
1305
|
+
</span>
|
|
1306
|
+
<Link
|
|
1307
|
+
to={page}
|
|
1308
|
+
className="mono ml-auto inline-flex shrink-0 items-center rounded-control border px-2 py-1 border-line-strong bg-canvas text-accent t-micro transition-colors hover:bg-raised"
|
|
1309
|
+
>
|
|
1310
|
+
{flowStep ? "open flow" : "open"}
|
|
1311
|
+
<span aria-hidden className="ml-1">
|
|
1312
|
+
→
|
|
1313
|
+
</span>
|
|
1314
|
+
</Link>
|
|
1315
|
+
</nav>
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1211
1319
|
export function DetailPanel() {
|
|
1212
1320
|
const selection = useSelectionStore((s) => s.selection);
|
|
1213
1321
|
const clear = useSelectionStore((s) => s.clear);
|
|
@@ -1225,34 +1333,33 @@ export function DetailPanel() {
|
|
|
1225
1333
|
className="panel-in pane flex h-full w-full flex-col border-l border-line bg-canvas"
|
|
1226
1334
|
aria-label="Selection detail"
|
|
1227
1335
|
>
|
|
1228
|
-
<div className="sticky-bar flex shrink-0 items-center gap-2 border-b px-4 py-2
|
|
1336
|
+
<div className="sticky-bar sticky top-0 z-10 flex shrink-0 items-center gap-2 border-b px-4 py-2 border-line">
|
|
1229
1337
|
<span className="label">{kindLabel(selection, resolved)}</span>
|
|
1230
1338
|
{resolved?.kind === "flow-step" ? (
|
|
1231
1339
|
<StatusChip status={resolved.step.status} />
|
|
1232
1340
|
) : null}
|
|
1233
|
-
{/*
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
</button>
|
|
1341
|
+
{/* Panel controls own the far edge even when this selection cannot be
|
|
1342
|
+
pinned. The close button used to sit immediately after the status
|
|
1343
|
+
in that case, which made it look attached to the status itself. */}
|
|
1344
|
+
<div className="ml-auto flex shrink-0 items-center gap-1">
|
|
1345
|
+
{pin ? <PinButton kind={pin.kind} id={pin.id} size={14} /> : null}
|
|
1346
|
+
<button
|
|
1347
|
+
type="button"
|
|
1348
|
+
onClick={() => clear("panel")}
|
|
1349
|
+
aria-label="Close selection detail (Esc)"
|
|
1350
|
+
title="Close · Esc"
|
|
1351
|
+
className="inline-flex size-7 items-center justify-center rounded-control border border-transparent text-muted t-micro transition-colors hover:border-line hover:bg-surface hover:text-ink"
|
|
1352
|
+
>
|
|
1353
|
+
<X size={16} aria-hidden />
|
|
1354
|
+
</button>
|
|
1355
|
+
</div>
|
|
1249
1356
|
</div>
|
|
1250
1357
|
|
|
1251
1358
|
<div className="flex-1 overflow-y-auto p-4">
|
|
1252
1359
|
<div className="flex flex-wrap items-baseline gap-x-2">
|
|
1253
|
-
<
|
|
1360
|
+
<h2 className="mono break-all text-sm font-medium text-ink">
|
|
1254
1361
|
{titleOf(resolved, selection)}
|
|
1255
|
-
</
|
|
1362
|
+
</h2>
|
|
1256
1363
|
{resolved?.kind === "event" ||
|
|
1257
1364
|
resolved?.kind === "service" ||
|
|
1258
1365
|
resolved?.kind === "aggregate" ? (
|
|
@@ -1274,14 +1381,7 @@ export function DetailPanel() {
|
|
|
1274
1381
|
<Ident block value={selection.id} className="mt-0.5 text-muted" />
|
|
1275
1382
|
) : null}
|
|
1276
1383
|
|
|
1277
|
-
{page
|
|
1278
|
-
<Link
|
|
1279
|
-
to={page}
|
|
1280
|
-
className="mono mt-3 inline-block rounded-control text-accent hover:underline"
|
|
1281
|
-
>
|
|
1282
|
-
open page →
|
|
1283
|
-
</Link>
|
|
1284
|
-
) : null}
|
|
1384
|
+
<DetailNavigation resolved={resolved} page={page} />
|
|
1285
1385
|
|
|
1286
1386
|
{resolved === null ? (
|
|
1287
1387
|
<UnknownBody selection={selection} />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { pageContains, selectionPath } from "./pages";
|
|
2
|
+
import { pageContains, selectionPath, selectsInPlace } from "./pages";
|
|
3
3
|
import { selectionFor } from "./model";
|
|
4
4
|
|
|
5
5
|
const dispatched = selectionFor("delivery.core.shipment.ShipmentDispatched");
|
|
@@ -44,6 +44,19 @@ describe("selectionPath", () => {
|
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
describe("selectsInPlace", () => {
|
|
48
|
+
const flow = "/flows/cart-checkout";
|
|
49
|
+
const shop = selectionFor("shop");
|
|
50
|
+
|
|
51
|
+
it("selects a flow context on the flow the first time", () => {
|
|
52
|
+
expect(selectsInPlace(flow, shop, null)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("lets a repeated selection follow its link to the context page", () => {
|
|
56
|
+
expect(selectsInPlace(flow, shop, shop)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
47
60
|
// Navigation keeps a selection only when the page it lands on can point at it.
|
|
48
61
|
// These are the cases that decide whether the palette moves the reader.
|
|
49
62
|
describe("pageContains across pages", () => {
|
package/src/selection/pages.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { walkSteps } from "../catalog";
|
|
|
9
9
|
import { catalog, index } from "../data";
|
|
10
10
|
import { paths } from "../routes";
|
|
11
11
|
import type { Selection } from "./model";
|
|
12
|
-
import { parseFlowStepId, resolveSelection } from "./model";
|
|
12
|
+
import { parseFlowStepId, resolveSelection, sameSelection } from "./model";
|
|
13
13
|
|
|
14
14
|
/** The page that owns a selection, or null when it has no page of its own. */
|
|
15
15
|
export function selectionPath(selection: Selection): string | null {
|
|
@@ -257,12 +257,18 @@ export function pageContains(pathname: string, selection: Selection): boolean {
|
|
|
257
257
|
* and only for something that flow actually draws. That is the one place where
|
|
258
258
|
* leaving is the wrong answer — picking an event there is a question about
|
|
259
259
|
* *this* sequence ("where does it appear?"), and the flow page answers it by
|
|
260
|
-
* lighting the matching steps.
|
|
260
|
+
* lighting the matching steps. Clicking the highlighted selection again is an
|
|
261
|
+
* explicit request to follow the tree link to its own page.
|
|
261
262
|
*/
|
|
262
263
|
export function selectsInPlace(
|
|
263
264
|
pathname: string,
|
|
264
265
|
selection: Selection,
|
|
266
|
+
current: Selection | null,
|
|
265
267
|
): boolean {
|
|
266
268
|
const route = parseRoute(pathname);
|
|
267
|
-
return
|
|
269
|
+
return (
|
|
270
|
+
!sameSelection(current, selection) &&
|
|
271
|
+
route.kind === "flow" &&
|
|
272
|
+
pageContains(pathname, selection)
|
|
273
|
+
);
|
|
268
274
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Browser navigation selects a catalog through `?catalog=`. Node has no URL,
|
|
4
|
+
// so tests that exercise the shipped example estate make that same choice by
|
|
5
|
+
// setting the fixture manifest's default profile explicitly.
|
|
6
|
+
vi.mock("../../portolan.json", async (importOriginal) => {
|
|
7
|
+
const module = await importOriginal<{ default: Record<string, unknown> }>();
|
|
8
|
+
return {
|
|
9
|
+
default: {
|
|
10
|
+
...module.default,
|
|
11
|
+
defaultCatalog: "example",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
});
|
package/vite.config.ts
CHANGED
|
@@ -7,6 +7,8 @@ import react from "@vitejs/plugin-react";
|
|
|
7
7
|
import tailwindcss from "@tailwindcss/vite";
|
|
8
8
|
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
|
9
9
|
import { publicSetupFrom } from "./src/lib/setup-info.ts";
|
|
10
|
+
// @ts-expect-error plain JavaScript module intentionally has no browser types
|
|
11
|
+
import { readManifest } from "./scripts/manifest.mjs";
|
|
10
12
|
// The local control plane is a Node-only Vite plugin kept as plain ESM so its
|
|
11
13
|
// pure discovery functions can also be tested directly.
|
|
12
14
|
// @ts-expect-error plain JavaScript module intentionally has no browser types
|
|
@@ -121,7 +123,7 @@ try {
|
|
|
121
123
|
// A clean checkout has no run to report yet. Settings says so explicitly.
|
|
122
124
|
}
|
|
123
125
|
const setupInfo = publicSetupFrom(
|
|
124
|
-
|
|
126
|
+
readManifest(resolve(workspace, "portolan.json")) as unknown,
|
|
125
127
|
buildReport,
|
|
126
128
|
createHash("sha256").update(manifestText).digest("hex"),
|
|
127
129
|
);
|