@shortlink-org/portolan 0.2.2 → 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/portolan.mjs +36 -6
- package/package.json +4 -4
- package/plugins/portolan-go.wasm +0 -0
- package/scripts/builtin-plugins.mjs +3 -2
- package/scripts/catalog-sources.mjs +2 -1
- package/scripts/catalog-sources.test.mjs +11 -1
- package/scripts/delivery-presets.mjs +207 -24
- package/scripts/diff.mjs +5 -1
- package/scripts/local-api.mjs +26 -377
- package/scripts/local-api.test.mjs +50 -0
- package/scripts/local-discovery.mjs +378 -0
- package/scripts/manifest.mjs +42 -1
- package/scripts/manifest.test.mjs +24 -1
- package/scripts/run-builtin.mjs +4 -2
- package/scripts/schema.mjs +4 -0
- package/scripts/site-docs.mjs +2 -2
- package/src/app/CatalogApp.tsx +4 -4
- 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/catalog-index.ts +485 -0
- package/src/catalog-model.ts +1339 -0
- package/src/catalog-validation.ts +1570 -0
- package/src/catalog.test.ts +16 -0
- package/src/catalog.ts +6 -3306
- package/src/components/{PageHeader.test.ts → PageHeader.test.tsx} +9 -10
- package/src/components/ProblemRow.tsx +3 -0
- package/src/components/SourcePreview.tsx +1 -1
- package/src/index.css +0 -17
- package/src/landing/LandingPage.tsx +4 -4
- package/src/lib/all-problems.ts +1 -1
- package/src/lib/derive.ts +1 -0
- package/src/lib/local-api.ts +19 -7
- package/src/lib/motion.test.ts +4 -2
- package/src/lib/motion.tsx +5 -4
- package/src/lib/proto-problems.test.ts +170 -3
- package/src/lib/proto-problems.ts +176 -4
- package/src/merge.test.ts +46 -0
- package/src/merge.ts +35 -1
- package/src/pages/Settings.tsx +1 -126
- package/src/pages/settings/AboutSettings.tsx +129 -0
- package/src/pages/settings/DeliverySettings.tsx +71 -15
- package/src/selection/pages.test.ts +14 -1
- package/src/selection/pages.ts +9 -3
- package/vite.config.ts +3 -1
package/src/merge.test.ts
CHANGED
|
@@ -181,6 +181,52 @@ describe("mergeCatalogs", () => {
|
|
|
181
181
|
expect(service?.provides.map((p) => p.id)).toEqual(["auth.v1.Users"]);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
it("retains and unions vendored proto descriptors across service fragments", () => {
|
|
185
|
+
const first = context("shop", ["shop.oms"]);
|
|
186
|
+
first.services[0]!.copies = [
|
|
187
|
+
{
|
|
188
|
+
id: "pricing.v1.Pricing",
|
|
189
|
+
source: "pricing.proto",
|
|
190
|
+
methods: [{ name: "GetQuote" }],
|
|
191
|
+
enums: [
|
|
192
|
+
{
|
|
193
|
+
name: "Status",
|
|
194
|
+
values: [{ name: "STATUS_UNSPECIFIED", number: 0 }],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
const second = context("shop", ["shop.oms"]);
|
|
200
|
+
second.services[0]!.copies = [
|
|
201
|
+
{
|
|
202
|
+
id: "pricing.v1.Pricing",
|
|
203
|
+
source: "pricing.proto",
|
|
204
|
+
methods: [{ name: "WatchPrices" }],
|
|
205
|
+
enums: [
|
|
206
|
+
{
|
|
207
|
+
name: "Status",
|
|
208
|
+
values: [{ name: "STATUS_READY", number: 1 }],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const merged = mergeCatalogs([
|
|
215
|
+
source("a.json", { contexts: [first] }),
|
|
216
|
+
source("b.json", { contexts: [second] }),
|
|
217
|
+
]);
|
|
218
|
+
const copy = merged.catalog.contexts[0]?.services[0]?.copies?.[0];
|
|
219
|
+
|
|
220
|
+
expect(copy?.methods.map((method) => method.name)).toEqual([
|
|
221
|
+
"GetQuote",
|
|
222
|
+
"WatchPrices",
|
|
223
|
+
]);
|
|
224
|
+
expect(copy?.enums?.[0]?.values.map((value) => value.name)).toEqual([
|
|
225
|
+
"STATUS_UNSPECIFIED",
|
|
226
|
+
"STATUS_READY",
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
229
|
+
|
|
184
230
|
it("unions a service's commands by the line to type", () => {
|
|
185
231
|
const domain = context("shop", ["shop.cart"]);
|
|
186
232
|
domain.services = domain.services.map((s) => ({
|
package/src/merge.ts
CHANGED
|
@@ -473,8 +473,11 @@ function mergeService(
|
|
|
473
473
|
if (!existing) {
|
|
474
474
|
services.set(incoming.id, {
|
|
475
475
|
...incoming,
|
|
476
|
-
provides:
|
|
476
|
+
provides: incoming.provides.map(copyInterface),
|
|
477
477
|
consumes: [...incoming.consumes],
|
|
478
|
+
...(incoming.copies
|
|
479
|
+
? { copies: incoming.copies.map(copyInterface) }
|
|
480
|
+
: {}),
|
|
478
481
|
aggregates: incoming.aggregates.map(copyAggregate),
|
|
479
482
|
...(incoming.stores ? { stores: [...incoming.stores] } : {}),
|
|
480
483
|
...(incoming.modules ? { modules: [...incoming.modules] } : {}),
|
|
@@ -534,6 +537,11 @@ function mergeService(
|
|
|
534
537
|
|
|
535
538
|
mergeInterfaces(existing.provides, incoming.provides);
|
|
536
539
|
appendNew(existing.consumes, incoming.consumes, (c) => c.id, raise);
|
|
540
|
+
if (incoming.copies?.length) {
|
|
541
|
+
const copies = existing.copies ?? [];
|
|
542
|
+
mergeInterfaces(copies, incoming.copies);
|
|
543
|
+
existing.copies = copies;
|
|
544
|
+
}
|
|
537
545
|
mergeAggregates(existing.aggregates, incoming.aggregates);
|
|
538
546
|
|
|
539
547
|
if (incoming.stores?.length) {
|
|
@@ -713,6 +721,24 @@ function mergeInterfaces(target: RpcService[], incoming: RpcService[]): void {
|
|
|
713
721
|
}
|
|
714
722
|
}
|
|
715
723
|
if (messages.length > 0) mine.messages = messages;
|
|
724
|
+
|
|
725
|
+
const enums = mine.enums ?? [];
|
|
726
|
+
for (const item of theirs.enums ?? []) {
|
|
727
|
+
const existing = enums.find((candidate) => candidate.name === item.name);
|
|
728
|
+
if (!existing) {
|
|
729
|
+
enums.push({
|
|
730
|
+
...item,
|
|
731
|
+
values: item.values.map((value) => ({ ...value })),
|
|
732
|
+
});
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
appendNew(
|
|
736
|
+
existing.values,
|
|
737
|
+
item.values.map((value) => ({ ...value })),
|
|
738
|
+
(value) => value.name,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
if (enums.length > 0) mine.enums = enums;
|
|
716
742
|
}
|
|
717
743
|
}
|
|
718
744
|
|
|
@@ -736,6 +762,14 @@ function copyInterface(provided: RpcService): RpcService {
|
|
|
736
762
|
})),
|
|
737
763
|
}
|
|
738
764
|
: {}),
|
|
765
|
+
...(provided.enums
|
|
766
|
+
? {
|
|
767
|
+
enums: provided.enums.map((item) => ({
|
|
768
|
+
...item,
|
|
769
|
+
values: item.values.map((value) => ({ ...value })),
|
|
770
|
+
})),
|
|
771
|
+
}
|
|
772
|
+
: {}),
|
|
739
773
|
};
|
|
740
774
|
}
|
|
741
775
|
|
package/src/pages/Settings.tsx
CHANGED
|
@@ -3,13 +3,10 @@ import { createContext, useCallback, useContext, useEffect, useState } from "rea
|
|
|
3
3
|
import { Link, Navigate, NavLink, Route, Routes, useLocation } from "react-router";
|
|
4
4
|
import {
|
|
5
5
|
ArrowLeft,
|
|
6
|
-
ArrowRight,
|
|
7
|
-
BookOpen,
|
|
8
6
|
Box,
|
|
9
7
|
Check,
|
|
10
8
|
ChevronDown,
|
|
11
9
|
CircleAlert,
|
|
12
|
-
CircleDot,
|
|
13
10
|
FolderGit2,
|
|
14
11
|
GitBranch,
|
|
15
12
|
KeyRound,
|
|
@@ -21,7 +18,6 @@ import {
|
|
|
21
18
|
Trash2,
|
|
22
19
|
X,
|
|
23
20
|
} from "lucide-react";
|
|
24
|
-
import type { Variants } from "motion/react";
|
|
25
21
|
import { catalog, catalogSources } from "../data";
|
|
26
22
|
import { useToastStore } from "../app/toast";
|
|
27
23
|
import { absoluteTime, plural, relativeTime } from "../lib/format";
|
|
@@ -59,15 +55,9 @@ import { Modal } from "../components/Overlay";
|
|
|
59
55
|
import { MachineDocs } from "../components/MachineDocs";
|
|
60
56
|
import { DeliverySettings } from "./settings/DeliverySettings";
|
|
61
57
|
import { PreferencesSettings } from "./settings/PreferencesSettings";
|
|
58
|
+
import { AboutSettings } from "./settings/AboutSettings";
|
|
62
59
|
import { CatEmptyState, CatIllustration } from "../components/CatIllustration";
|
|
63
60
|
import { CommitLink } from "../components/CommitLink";
|
|
64
|
-
import { m, transitions } from "../lib/motion";
|
|
65
|
-
import {
|
|
66
|
-
PRODUCT_ISSUES,
|
|
67
|
-
PRODUCT_LICENSE,
|
|
68
|
-
PRODUCT_README,
|
|
69
|
-
PRODUCT_REPOSITORY,
|
|
70
|
-
} from "../lib/product";
|
|
71
61
|
|
|
72
62
|
type Health = "healthy" | "changed" | "failed" | "unchecked";
|
|
73
63
|
type ProjectSource = ProjectDraft["source"];
|
|
@@ -1117,121 +1107,6 @@ function PipelineSettings() {
|
|
|
1117
1107
|
);
|
|
1118
1108
|
}
|
|
1119
1109
|
|
|
1120
|
-
const ABOUT_CARD_MOTION: Variants = {
|
|
1121
|
-
hidden: { opacity: 0, y: 8 },
|
|
1122
|
-
shown: {
|
|
1123
|
-
opacity: 1,
|
|
1124
|
-
y: 0,
|
|
1125
|
-
transition: { ...transitions.page, staggerChildren: 0.05 },
|
|
1126
|
-
},
|
|
1127
|
-
hover: {},
|
|
1128
|
-
};
|
|
1129
|
-
|
|
1130
|
-
const ABOUT_COPY_MOTION: Variants = {
|
|
1131
|
-
hidden: { opacity: 0, y: 6 },
|
|
1132
|
-
shown: { opacity: 1, y: 0, transition: transitions.panel },
|
|
1133
|
-
};
|
|
1134
|
-
|
|
1135
|
-
const ABOUT_CAT_MOTION: Variants = {
|
|
1136
|
-
hidden: { opacity: 0, x: 18, rotate: 3 },
|
|
1137
|
-
shown: {
|
|
1138
|
-
opacity: 1,
|
|
1139
|
-
x: 0,
|
|
1140
|
-
rotate: 0,
|
|
1141
|
-
transition: transitions.narrative,
|
|
1142
|
-
},
|
|
1143
|
-
hover: {
|
|
1144
|
-
y: -7,
|
|
1145
|
-
rotate: -2,
|
|
1146
|
-
scale: 1.035,
|
|
1147
|
-
transition: transitions.settle,
|
|
1148
|
-
},
|
|
1149
|
-
};
|
|
1150
|
-
|
|
1151
|
-
function AboutSettings() {
|
|
1152
|
-
return (
|
|
1153
|
-
<section>
|
|
1154
|
-
<div className="grid gap-grid lg:grid-cols-[minmax(0,1.25fr)_minmax(17rem,0.75fr)]">
|
|
1155
|
-
<m.div
|
|
1156
|
-
data-about-card
|
|
1157
|
-
className="card grid items-center gap-4 sm:grid-cols-[minmax(0,1fr)_180px]"
|
|
1158
|
-
variants={ABOUT_CARD_MOTION}
|
|
1159
|
-
initial="hidden"
|
|
1160
|
-
animate="shown"
|
|
1161
|
-
whileHover="hover"
|
|
1162
|
-
>
|
|
1163
|
-
<m.div
|
|
1164
|
-
className="flex flex-col items-start"
|
|
1165
|
-
variants={ABOUT_COPY_MOTION}
|
|
1166
|
-
>
|
|
1167
|
-
<p className="max-w-prose text-muted">
|
|
1168
|
-
Architecture from code and evidence. Product source,
|
|
1169
|
-
documentation, releases and issue tracking live in the public
|
|
1170
|
-
GitHub repository.
|
|
1171
|
-
</p>
|
|
1172
|
-
<div className="mt-5 flex flex-wrap gap-2">
|
|
1173
|
-
<Link to={paths.landing()} className="product-primary">
|
|
1174
|
-
Product page <ArrowRight size={15} aria-hidden />
|
|
1175
|
-
</Link>
|
|
1176
|
-
<a
|
|
1177
|
-
href={PRODUCT_REPOSITORY}
|
|
1178
|
-
target="_blank"
|
|
1179
|
-
rel="noreferrer"
|
|
1180
|
-
className="tbtn text-ink"
|
|
1181
|
-
>
|
|
1182
|
-
<GitBranch size={15} aria-hidden /> View product on GitHub
|
|
1183
|
-
</a>
|
|
1184
|
-
</div>
|
|
1185
|
-
</m.div>
|
|
1186
|
-
<m.div
|
|
1187
|
-
data-about-cat
|
|
1188
|
-
className="h-44 w-full justify-self-center sm:h-52"
|
|
1189
|
-
variants={ABOUT_CAT_MOTION}
|
|
1190
|
-
>
|
|
1191
|
-
<CatIllustration scene="about" className="h-full w-full" />
|
|
1192
|
-
</m.div>
|
|
1193
|
-
</m.div>
|
|
1194
|
-
<m.div
|
|
1195
|
-
className="card"
|
|
1196
|
-
initial={{ opacity: 0, y: 8 }}
|
|
1197
|
-
animate={{ opacity: 1, y: 0 }}
|
|
1198
|
-
transition={{ ...transitions.page, delay: 0.06 }}
|
|
1199
|
-
>
|
|
1200
|
-
<div className="label mb-2">resources</div>
|
|
1201
|
-
<div className="divide-y divide-line">
|
|
1202
|
-
<a
|
|
1203
|
-
href={PRODUCT_README}
|
|
1204
|
-
target="_blank"
|
|
1205
|
-
rel="noreferrer"
|
|
1206
|
-
className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
|
|
1207
|
-
>
|
|
1208
|
-
<BookOpen size={15} aria-hidden className="shrink-0" />
|
|
1209
|
-
Documentation
|
|
1210
|
-
</a>
|
|
1211
|
-
<a
|
|
1212
|
-
href={PRODUCT_ISSUES}
|
|
1213
|
-
target="_blank"
|
|
1214
|
-
rel="noreferrer"
|
|
1215
|
-
className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
|
|
1216
|
-
>
|
|
1217
|
-
<CircleDot size={15} aria-hidden className="shrink-0" />
|
|
1218
|
-
Issues and feedback
|
|
1219
|
-
</a>
|
|
1220
|
-
<a
|
|
1221
|
-
href={PRODUCT_LICENSE}
|
|
1222
|
-
target="_blank"
|
|
1223
|
-
rel="noreferrer"
|
|
1224
|
-
className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
|
|
1225
|
-
>
|
|
1226
|
-
<ShieldCheck size={15} aria-hidden className="shrink-0" />
|
|
1227
|
-
MIT License
|
|
1228
|
-
</a>
|
|
1229
|
-
</div>
|
|
1230
|
-
</m.div>
|
|
1231
|
-
</div>
|
|
1232
|
-
</section>
|
|
1233
|
-
);
|
|
1234
|
-
}
|
|
1235
1110
|
|
|
1236
1111
|
function SettingsContent({ local, onAdd, onRemove, onGenerate }: { local: boolean; onAdd: (source: ProjectSource) => void; onRemove: (project: SetupProject) => void; onGenerate: () => void }) {
|
|
1237
1112
|
const pathname = useLocation().pathname.replace(/\/$/, "");
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ArrowRight, BookOpen, CircleDot, GitBranch, ShieldCheck } from "lucide-react";
|
|
2
|
+
import type { Variants } from "motion/react";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
|
|
5
|
+
import { CatIllustration } from "../../components/CatIllustration";
|
|
6
|
+
import { m, transitions } from "../../lib/motion";
|
|
7
|
+
import {
|
|
8
|
+
PRODUCT_ISSUES,
|
|
9
|
+
PRODUCT_LICENSE,
|
|
10
|
+
PRODUCT_README,
|
|
11
|
+
PRODUCT_REPOSITORY,
|
|
12
|
+
} from "../../lib/product";
|
|
13
|
+
import { paths } from "../../routes";
|
|
14
|
+
|
|
15
|
+
const ABOUT_CARD_MOTION: Variants = {
|
|
16
|
+
hidden: { opacity: 0, y: 8 },
|
|
17
|
+
shown: {
|
|
18
|
+
opacity: 1,
|
|
19
|
+
y: 0,
|
|
20
|
+
transition: { ...transitions.page, staggerChildren: 0.05 },
|
|
21
|
+
},
|
|
22
|
+
hover: {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ABOUT_COPY_MOTION: Variants = {
|
|
26
|
+
hidden: { opacity: 0, y: 6 },
|
|
27
|
+
shown: { opacity: 1, y: 0, transition: transitions.panel },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ABOUT_CAT_MOTION: Variants = {
|
|
31
|
+
hidden: { opacity: 0, x: 18, rotate: 3 },
|
|
32
|
+
shown: {
|
|
33
|
+
opacity: 1,
|
|
34
|
+
x: 0,
|
|
35
|
+
rotate: 0,
|
|
36
|
+
transition: transitions.narrative,
|
|
37
|
+
},
|
|
38
|
+
hover: {
|
|
39
|
+
y: -7,
|
|
40
|
+
rotate: -2,
|
|
41
|
+
scale: 1.035,
|
|
42
|
+
transition: transitions.settle,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function AboutSettings() {
|
|
47
|
+
return (
|
|
48
|
+
<section>
|
|
49
|
+
<div className="grid gap-grid lg:grid-cols-[minmax(0,1.25fr)_minmax(17rem,0.75fr)]">
|
|
50
|
+
<m.div
|
|
51
|
+
data-about-card
|
|
52
|
+
className="card grid items-center gap-4 sm:grid-cols-[minmax(0,1fr)_180px]"
|
|
53
|
+
variants={ABOUT_CARD_MOTION}
|
|
54
|
+
initial="hidden"
|
|
55
|
+
animate="shown"
|
|
56
|
+
whileHover="hover"
|
|
57
|
+
>
|
|
58
|
+
<m.div
|
|
59
|
+
className="flex flex-col items-start"
|
|
60
|
+
variants={ABOUT_COPY_MOTION}
|
|
61
|
+
>
|
|
62
|
+
<p className="max-w-prose text-muted">
|
|
63
|
+
Architecture from code and evidence. Product source,
|
|
64
|
+
documentation, releases and issue tracking live in the public
|
|
65
|
+
GitHub repository.
|
|
66
|
+
</p>
|
|
67
|
+
<div className="mt-5 flex flex-wrap gap-2">
|
|
68
|
+
<Link to={paths.landing()} className="product-primary">
|
|
69
|
+
Product page <ArrowRight size={15} aria-hidden />
|
|
70
|
+
</Link>
|
|
71
|
+
<a
|
|
72
|
+
href={PRODUCT_REPOSITORY}
|
|
73
|
+
target="_blank"
|
|
74
|
+
rel="noreferrer"
|
|
75
|
+
className="tbtn text-ink"
|
|
76
|
+
>
|
|
77
|
+
<GitBranch size={15} aria-hidden /> View product on GitHub
|
|
78
|
+
</a>
|
|
79
|
+
</div>
|
|
80
|
+
</m.div>
|
|
81
|
+
<m.div
|
|
82
|
+
data-about-cat
|
|
83
|
+
className="h-44 w-full justify-self-center sm:h-52"
|
|
84
|
+
variants={ABOUT_CAT_MOTION}
|
|
85
|
+
>
|
|
86
|
+
<CatIllustration scene="about" className="h-full w-full" />
|
|
87
|
+
</m.div>
|
|
88
|
+
</m.div>
|
|
89
|
+
<m.div
|
|
90
|
+
className="card"
|
|
91
|
+
initial={{ opacity: 0, y: 8 }}
|
|
92
|
+
animate={{ opacity: 1, y: 0 }}
|
|
93
|
+
transition={{ ...transitions.page, delay: 0.06 }}
|
|
94
|
+
>
|
|
95
|
+
<div className="label mb-2">resources</div>
|
|
96
|
+
<div className="divide-y divide-line">
|
|
97
|
+
<a
|
|
98
|
+
href={PRODUCT_README}
|
|
99
|
+
target="_blank"
|
|
100
|
+
rel="noreferrer"
|
|
101
|
+
className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
|
|
102
|
+
>
|
|
103
|
+
<BookOpen size={15} aria-hidden className="shrink-0" />
|
|
104
|
+
Documentation
|
|
105
|
+
</a>
|
|
106
|
+
<a
|
|
107
|
+
href={PRODUCT_ISSUES}
|
|
108
|
+
target="_blank"
|
|
109
|
+
rel="noreferrer"
|
|
110
|
+
className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
|
|
111
|
+
>
|
|
112
|
+
<CircleDot size={15} aria-hidden className="shrink-0" />
|
|
113
|
+
Issues and feedback
|
|
114
|
+
</a>
|
|
115
|
+
<a
|
|
116
|
+
href={PRODUCT_LICENSE}
|
|
117
|
+
target="_blank"
|
|
118
|
+
rel="noreferrer"
|
|
119
|
+
className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
|
|
120
|
+
>
|
|
121
|
+
<ShieldCheck size={15} aria-hidden className="shrink-0" />
|
|
122
|
+
MIT License
|
|
123
|
+
</a>
|
|
124
|
+
</div>
|
|
125
|
+
</m.div>
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { Switch } from "@headlessui/react";
|
|
2
3
|
import {
|
|
3
|
-
Check,
|
|
4
4
|
ChevronDown,
|
|
5
5
|
CircleAlert,
|
|
6
6
|
FolderGit2,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
previewDeliveryPreset,
|
|
15
15
|
} from "../../lib/local-api";
|
|
16
16
|
import type {
|
|
17
|
+
DeliveryFeatureId,
|
|
17
18
|
DeliveryPreset,
|
|
18
19
|
DeliveryProvider,
|
|
19
20
|
} from "../../lib/local-api";
|
|
@@ -24,6 +25,7 @@ const STATUS_CLASS: Record<
|
|
|
24
25
|
> = {
|
|
25
26
|
added: "status-verified",
|
|
26
27
|
changed: "status-declared",
|
|
28
|
+
removed: "status-unresolved",
|
|
27
29
|
unchanged: "status-verified",
|
|
28
30
|
conflict: "status-unresolved",
|
|
29
31
|
};
|
|
@@ -35,12 +37,12 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
35
37
|
const [error, setError] = useState("");
|
|
36
38
|
const say = useToastStore((state) => state.say);
|
|
37
39
|
|
|
38
|
-
const load = useCallback(async (provider?: DeliveryProvider) => {
|
|
40
|
+
const load = useCallback(async (provider?: DeliveryProvider, features?: DeliveryFeatureId[]) => {
|
|
39
41
|
if (!local) return;
|
|
40
42
|
setLoading(true);
|
|
41
43
|
setError("");
|
|
42
44
|
try {
|
|
43
|
-
setPlan(await previewDeliveryPreset(provider));
|
|
45
|
+
setPlan(await previewDeliveryPreset(provider, features));
|
|
44
46
|
} catch (cause) {
|
|
45
47
|
setError(cause instanceof Error ? cause.message : String(cause));
|
|
46
48
|
} finally {
|
|
@@ -60,6 +62,7 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
60
62
|
const installed = await installDeliveryPreset(
|
|
61
63
|
plan.provider,
|
|
62
64
|
plan.revision,
|
|
65
|
+
plan.features.filter((feature) => feature.selected).map((feature) => feature.id),
|
|
63
66
|
);
|
|
64
67
|
setPlan(installed);
|
|
65
68
|
say(
|
|
@@ -76,6 +79,23 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
function selectFeature(id: DeliveryFeatureId) {
|
|
83
|
+
if (!plan) return;
|
|
84
|
+
const feature = plan.features.find((item) => item.id === id);
|
|
85
|
+
if (!feature?.available) return;
|
|
86
|
+
const selected = new Set(
|
|
87
|
+
plan.features.filter((item) => item.selected).map((item) => item.id),
|
|
88
|
+
);
|
|
89
|
+
if (selected.has(id)) {
|
|
90
|
+
selected.delete(id);
|
|
91
|
+
if (id === "diff") selected.delete("sarif");
|
|
92
|
+
} else {
|
|
93
|
+
selected.add(id);
|
|
94
|
+
for (const required of feature.requires) selected.add(required);
|
|
95
|
+
}
|
|
96
|
+
void load(plan.provider, [...selected]);
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
if (!local) {
|
|
80
100
|
return (
|
|
81
101
|
<div className="empty">
|
|
@@ -96,8 +116,8 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
96
116
|
<FolderGit2 size={16} className="text-accent" /> Delivery automation
|
|
97
117
|
</div>
|
|
98
118
|
<p className="mt-1 max-w-prose text-muted">
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
Choose the automation this repository needs, preview the generated
|
|
120
|
+
jobs, then install them together.
|
|
101
121
|
</p>
|
|
102
122
|
</div>
|
|
103
123
|
<div
|
|
@@ -111,7 +131,12 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
111
131
|
type="button"
|
|
112
132
|
disabled={loading || installing}
|
|
113
133
|
aria-pressed={plan?.provider === provider}
|
|
114
|
-
onClick={() => void load(
|
|
134
|
+
onClick={() => void load(
|
|
135
|
+
provider,
|
|
136
|
+
plan?.features
|
|
137
|
+
.filter((feature) => feature.selected && (feature.id !== "sarif" || provider === "github"))
|
|
138
|
+
.map((feature) => feature.id),
|
|
139
|
+
)}
|
|
115
140
|
className={`capitalize ${plan?.provider === provider ? "is-on" : ""}`}
|
|
116
141
|
>
|
|
117
142
|
{provider === "github" ? "GitHub" : "GitLab"}
|
|
@@ -157,15 +182,46 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
157
182
|
)}
|
|
158
183
|
</div>
|
|
159
184
|
|
|
160
|
-
<
|
|
185
|
+
<div className="mt-4 grid gap-2 sm:grid-cols-2">
|
|
161
186
|
{plan.features.map((feature) => (
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
187
|
+
<div
|
|
188
|
+
key={feature.id}
|
|
189
|
+
className={`flex items-start gap-3 rounded-control border px-3 py-3 transition-colors ${
|
|
190
|
+
feature.available
|
|
191
|
+
? feature.selected
|
|
192
|
+
? "border-accent bg-surface"
|
|
193
|
+
: "border-line hover:bg-surface"
|
|
194
|
+
: "border-line opacity-60"
|
|
195
|
+
}`}
|
|
196
|
+
>
|
|
197
|
+
<span className="min-w-0 flex-1">
|
|
198
|
+
<span className="block font-medium text-ink">{feature.label}</span>
|
|
199
|
+
<span className="mt-0.5 block text-muted">{feature.description}</span>
|
|
200
|
+
{!feature.available ? (
|
|
201
|
+
<span className="mt-1 block text-faint">Available for GitHub only.</span>
|
|
202
|
+
) : feature.requires.length > 0 ? (
|
|
203
|
+
<span className="mt-1 block text-faint">
|
|
204
|
+
Enables {feature.requires.map((required) => plan.features.find((item) => item.id === required)?.label).join(", ")}.
|
|
205
|
+
</span>
|
|
206
|
+
) : null}
|
|
207
|
+
</span>
|
|
208
|
+
<Switch
|
|
209
|
+
checked={feature.selected}
|
|
210
|
+
disabled={!feature.available || loading || installing}
|
|
211
|
+
onChange={() => selectFeature(feature.id)}
|
|
212
|
+
aria-label={feature.label}
|
|
213
|
+
className="group mt-0.5 inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-line-strong bg-surface transition-colors outline-none data-checked:border-accent data-checked:bg-accent focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed"
|
|
214
|
+
>
|
|
215
|
+
<span
|
|
216
|
+
aria-hidden
|
|
217
|
+
className="size-3.5 translate-x-0.5 rounded-full bg-muted shadow-xs transition-transform group-data-checked:translate-x-[18px] group-data-checked:bg-canvas"
|
|
218
|
+
/>
|
|
219
|
+
</Switch>
|
|
220
|
+
</div>
|
|
165
221
|
))}
|
|
166
|
-
</
|
|
222
|
+
</div>
|
|
167
223
|
|
|
168
|
-
<div className="mt-4 divide-y divide-line overflow-hidden rounded-control border border-line">
|
|
224
|
+
{plan.files.length > 0 ? <div className="mt-4 divide-y divide-line overflow-hidden rounded-control border border-line">
|
|
169
225
|
{plan.files.map((file) => (
|
|
170
226
|
<details
|
|
171
227
|
key={file.path}
|
|
@@ -194,7 +250,7 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
194
250
|
</div>
|
|
195
251
|
</details>
|
|
196
252
|
))}
|
|
197
|
-
</div>
|
|
253
|
+
</div> : <div className="empty mt-4">No delivery jobs selected.</div>}
|
|
198
254
|
|
|
199
255
|
{error ? <p className="mt-3 text-unresolved">{error}</p> : null}
|
|
200
256
|
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
|
|
@@ -204,12 +260,12 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
204
260
|
</p>
|
|
205
261
|
{plan.status === "installed" ? (
|
|
206
262
|
<span className="flex items-center gap-1.5 text-verified">
|
|
207
|
-
<ShieldCheck size={15} />
|
|
263
|
+
<ShieldCheck size={15} /> Selection installed
|
|
208
264
|
</span>
|
|
209
265
|
) : (
|
|
210
266
|
<button
|
|
211
267
|
type="button"
|
|
212
|
-
className="
|
|
268
|
+
className="product-primary"
|
|
213
269
|
disabled={installing || conflicts.length > 0}
|
|
214
270
|
onClick={() => void install()}
|
|
215
271
|
>
|
|
@@ -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
|
}
|
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
|
);
|