@arcote.tech/arc-process 0.7.28 → 0.7.29
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/package.json +5 -4
- package/src/layout/layout.ts +60 -9
- package/src/mdx/arc.tsx +50 -10
- package/src/mdx/contexts.tsx +11 -0
- package/src/mdx/mdx-view.tsx +4 -0
- package/src/mdx/pin.tsx +101 -0
- package/src/model/build.ts +73 -22
- package/src/model/collapse.ts +2 -0
- package/src/model/profile.ts +15 -0
- package/src/model/types.ts +37 -0
- package/src/profiles/arc.ts +29 -1
- package/src/react/drawer.tsx +374 -0
- package/src/react/index.ts +5 -0
- package/src/react/nodes.tsx +65 -10
- package/src/react/process-diagram.tsx +47 -4
- package/tests/build.test.ts +98 -4
- package/tests/layout.test.ts +34 -0
|
@@ -44,10 +44,12 @@ import { MdxView, parseProcessFrontmatter } from "../mdx/mdx-view";
|
|
|
44
44
|
import {
|
|
45
45
|
FocusContext,
|
|
46
46
|
ProcessDataContext,
|
|
47
|
+
SelectionContext,
|
|
47
48
|
ViewStateContext,
|
|
48
49
|
} from "../mdx/contexts";
|
|
49
50
|
import { makeNodeTypes } from "./nodes";
|
|
50
|
-
import {
|
|
51
|
+
import { ProcessDrawer, type SourceProvider } from "./drawer";
|
|
52
|
+
import { GLASS } from "./theme";
|
|
51
53
|
|
|
52
54
|
export interface ProcessDiagramProps {
|
|
53
55
|
/** Surowy MDX procesu. */
|
|
@@ -70,6 +72,8 @@ export interface ProcessDiagramProps {
|
|
|
70
72
|
defaultCollapsed?: boolean;
|
|
71
73
|
/** Id/nazwa kontekstu domowego (fallback: frontmatter `context:`). */
|
|
72
74
|
homeContextId?: string;
|
|
75
|
+
/** Dostawca kodu źródłowego do drawera (np. fetch /api/source w arc-map). */
|
|
76
|
+
sourceProvider?: SourceProvider;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
function rfNode(n: DiagramNode): Node {
|
|
@@ -115,11 +119,13 @@ export function ProcessDiagram({
|
|
|
115
119
|
title,
|
|
116
120
|
defaultCollapsed,
|
|
117
121
|
homeContextId,
|
|
122
|
+
sourceProvider,
|
|
118
123
|
}: ProcessDiagramProps) {
|
|
119
124
|
const [occurrences, setOccurrences] = useState<Occurrence[]>([]);
|
|
120
125
|
const [viewState, setViewState] = useState<ProcessViewState | null>(null);
|
|
121
126
|
const [focusNodeId, setFocusNodeId] = useState<string | null>(null);
|
|
122
127
|
const [scrollToKey, setScrollToKey] = useState<string | null>(null);
|
|
128
|
+
const [selectedOcc, setSelectedOcc] = useState<string | null>(null);
|
|
123
129
|
const proseRef = useRef<HTMLDivElement>(null);
|
|
124
130
|
|
|
125
131
|
const frontmatter = useMemo(() => parseProcessFrontmatter(mdx), [mdx]);
|
|
@@ -132,6 +138,7 @@ export function ProcessDiagram({
|
|
|
132
138
|
useEffect(() => {
|
|
133
139
|
setOccurrences([]);
|
|
134
140
|
setViewState(null);
|
|
141
|
+
setSelectedOcc(null);
|
|
135
142
|
}, [mdx]);
|
|
136
143
|
|
|
137
144
|
const model = useMemo(
|
|
@@ -218,15 +225,35 @@ export function ProcessDiagram({
|
|
|
218
225
|
|
|
219
226
|
const processData = useMemo(() => ({ graph, profile }), [graph, profile]);
|
|
220
227
|
|
|
228
|
+
const selectionState = useMemo(
|
|
229
|
+
() => ({ selectedOcc, selectOcc: setSelectedOcc }),
|
|
230
|
+
[selectedOcc],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Krok do drawera: wybrany occId szukamy najpierw wśród kroków po collapse
|
|
234
|
+
// (pseudo-kroki boxów), potem w oryginalnych (chip prozy członka boxa).
|
|
235
|
+
const selectedStep = useMemo(() => {
|
|
236
|
+
if (!selectedOcc) return null;
|
|
237
|
+
const group = collapse.groupByOcc.get(selectedOcc);
|
|
238
|
+
return (
|
|
239
|
+
collapse.steps.find((s) => s.occId === (group ?? selectedOcc)) ??
|
|
240
|
+
model.steps.find((s) => s.occId === selectedOcc) ??
|
|
241
|
+
null
|
|
242
|
+
);
|
|
243
|
+
}, [selectedOcc, collapse, model]);
|
|
244
|
+
|
|
221
245
|
return (
|
|
222
246
|
<ProcessDataContext.Provider value={processData}>
|
|
223
247
|
<FocusContext.Provider value={focusState}>
|
|
248
|
+
<SelectionContext.Provider value={selectionState}>
|
|
224
249
|
<ViewStateContext.Provider value={viewStateActions}>
|
|
225
250
|
<div
|
|
226
251
|
style={{
|
|
227
252
|
display: "grid",
|
|
228
253
|
gridTemplateColumns: "minmax(320px, 1fr) minmax(420px, 1.3fr)",
|
|
229
254
|
height: "100%",
|
|
255
|
+
minHeight: 0,
|
|
256
|
+
overflow: "hidden",
|
|
230
257
|
}}
|
|
231
258
|
>
|
|
232
259
|
{/* Proza */}
|
|
@@ -234,6 +261,7 @@ export function ProcessDiagram({
|
|
|
234
261
|
ref={proseRef}
|
|
235
262
|
style={{
|
|
236
263
|
overflow: "auto",
|
|
264
|
+
minHeight: 0,
|
|
237
265
|
padding: "20px 24px",
|
|
238
266
|
borderRight: `1px solid ${GLASS.border}`,
|
|
239
267
|
}}
|
|
@@ -241,8 +269,8 @@ export function ProcessDiagram({
|
|
|
241
269
|
<MdxView key={docKey} mdx={mdx} onOccurrences={setOccurrences} />
|
|
242
270
|
</div>
|
|
243
271
|
|
|
244
|
-
{/* Diagram */}
|
|
245
|
-
<div style={{ position: "relative" }}>
|
|
272
|
+
{/* Diagram — sztywno wypełnia dostępną wysokość, bez scrolla strony */}
|
|
273
|
+
<div style={{ position: "relative", minHeight: 0, overflow: "hidden" }}>
|
|
246
274
|
<ReactFlow
|
|
247
275
|
key={docKey}
|
|
248
276
|
nodes={nodes}
|
|
@@ -260,7 +288,10 @@ export function ProcessDiagram({
|
|
|
260
288
|
onNodeClick={(_, n) => {
|
|
261
289
|
const k = keyByOcc.get(n.id);
|
|
262
290
|
if (k) setScrollToKey(k);
|
|
291
|
+
// Kroki (także boxy kontekstów) otwierają drawer.
|
|
292
|
+
if (n.type === "step") setSelectedOcc(n.id);
|
|
263
293
|
}}
|
|
294
|
+
onPaneClick={() => setSelectedOcc(null)}
|
|
264
295
|
fitView
|
|
265
296
|
fitViewOptions={{ padding: 0.18 }}
|
|
266
297
|
minZoom={0.1}
|
|
@@ -273,9 +304,10 @@ export function ProcessDiagram({
|
|
|
273
304
|
zoomable
|
|
274
305
|
nodeColor={(n) => {
|
|
275
306
|
const step = (n.data as { step?: ProcessStep }).step;
|
|
276
|
-
|
|
307
|
+
// Kolor z zadeklarowanego rodzaju — także dla TODO.
|
|
277
308
|
const kind =
|
|
278
309
|
step?.node?.kind ??
|
|
310
|
+
step?.ref?.kind ??
|
|
279
311
|
(n.data as { node?: { kind: string } }).node?.kind;
|
|
280
312
|
return kind ? kindColor(profile, kind) : "#64748b";
|
|
281
313
|
}}
|
|
@@ -286,9 +318,20 @@ export function ProcessDiagram({
|
|
|
286
318
|
}}
|
|
287
319
|
/>
|
|
288
320
|
</ReactFlow>
|
|
321
|
+
|
|
322
|
+
{/* Drawer szczegółów wybranego kroku. */}
|
|
323
|
+
{selectedStep ? (
|
|
324
|
+
<ProcessDrawer
|
|
325
|
+
step={selectedStep}
|
|
326
|
+
profile={profile}
|
|
327
|
+
onClose={() => setSelectedOcc(null)}
|
|
328
|
+
sourceProvider={sourceProvider}
|
|
329
|
+
/>
|
|
330
|
+
) : null}
|
|
289
331
|
</div>
|
|
290
332
|
</div>
|
|
291
333
|
</ViewStateContext.Provider>
|
|
334
|
+
</SelectionContext.Provider>
|
|
292
335
|
</FocusContext.Provider>
|
|
293
336
|
</ProcessDataContext.Provider>
|
|
294
337
|
);
|
package/tests/build.test.ts
CHANGED
|
@@ -239,10 +239,13 @@ describe("buildProcessModel — fazy, Entry/Continue, notatki, konteksty", () =>
|
|
|
239
239
|
const m = buildProcessModel({ graph, profile: arcProfile, occurrences });
|
|
240
240
|
const by = new Map(m.steps.map((s) => [s.occId, s]));
|
|
241
241
|
|
|
242
|
-
// Oś główna
|
|
243
|
-
|
|
244
|
-
expect([by.get("
|
|
245
|
-
|
|
242
|
+
// Oś główna przesunięta o wyprzedzenie wejścia (maxLead = 2 − 0 = 2),
|
|
243
|
+
// by łańcuch Entry zmieścił się PRZED fazą — continues idzie w prawo.
|
|
244
|
+
expect([by.get("m1")!.depth, by.get("m1")!.lane]).toEqual([2, 0]);
|
|
245
|
+
expect([by.get("m2")!.depth, by.get("m2")!.lane]).toEqual([3, 0]);
|
|
246
|
+
// Entry (łańcuch L=2, cel depth 2) → depths 0,1 — koniec kolumnę przed fazą.
|
|
247
|
+
expect(by.get("e1")!.depth).toBe(0);
|
|
248
|
+
expect(by.get("e2")!.depth).toBe(1);
|
|
246
249
|
expect(by.get("e1")!.lane).toBeLessThan(0);
|
|
247
250
|
expect(by.get("e2")!.lane).toBe(by.get("e1")!.lane);
|
|
248
251
|
expect(by.get("e1")!.entryTitle).toBe("Formularz");
|
|
@@ -294,6 +297,46 @@ describe("buildProcessModel — fazy, Entry/Continue, notatki, konteksty", () =>
|
|
|
294
297
|
expect(m.continueEdges).toEqual([{ fromOcc: "f1", toOcc: "m1", phaseId: "" }]);
|
|
295
298
|
});
|
|
296
299
|
|
|
300
|
+
test("maxLead: wejście w środek nie przesuwa osi, gdy faza jest wystarczająco głęboko", () => {
|
|
301
|
+
const occurrences: Occurrence[] = [
|
|
302
|
+
occ("m1", { view: "v" }, "m1", phasePath("start")),
|
|
303
|
+
occ("m2", { view: "x" }, "m2", phasePath("wywiad")),
|
|
304
|
+
occ("a1", { command: "cmd" }, "a1", entryPath("en-a", "Admin")),
|
|
305
|
+
{ type: "continue", to: "wywiad", path: entryPath("en-a", "Admin") },
|
|
306
|
+
];
|
|
307
|
+
const m = buildProcessModel({ graph, profile: arcProfile, occurrences });
|
|
308
|
+
const by = new Map(m.steps.map((s) => [s.occId, s]));
|
|
309
|
+
// Faza wywiad na depth 1, łańcuch L=1 → lead 0 → oś bez przesunięcia.
|
|
310
|
+
expect(by.get("m1")!.depth).toBe(0);
|
|
311
|
+
expect(by.get("m2")!.depth).toBe(1);
|
|
312
|
+
expect(by.get("a1")!.depth).toBe(0); // kolumnę przed fazą wywiad
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("tory wejść: cel dalej w prawo = bliżej osi (mniej przecięć)", () => {
|
|
316
|
+
const occurrences: Occurrence[] = [
|
|
317
|
+
// Dwa wejścia NAD osią (indeksy 0 i 2 w kolejności dokumentu).
|
|
318
|
+
occ("f1", { command: "cmd" }, "f1", entryPath("en-a", "A")),
|
|
319
|
+
{ type: "continue", to: "p1", path: entryPath("en-a", "A") },
|
|
320
|
+
occ("g1", { listener: "l" }, "g1", entryPath("en-b", "B")),
|
|
321
|
+
{ type: "continue", to: "p2", path: entryPath("en-b", "B") },
|
|
322
|
+
occ("h1", { view: "x" }, "h1", entryPath("en-c", "C")),
|
|
323
|
+
{ type: "continue", to: "p1", path: entryPath("en-c", "C") },
|
|
324
|
+
occ("m1", { view: "v" }, "m1", phasePath("p1")),
|
|
325
|
+
occ("m2", { event: "e" }, "m2", phasePath("p2")),
|
|
326
|
+
];
|
|
327
|
+
const m = buildProcessModel({ graph, profile: arcProfile, occurrences });
|
|
328
|
+
const by = new Map(m.steps.map((s) => [s.occId, s]));
|
|
329
|
+
// Nad osią: f1 (cel p1) i h1 (cel p1)... f1(k=0) i h1(k=2) nad osią,
|
|
330
|
+
// g1(k=1) pod osią. Nad osią cel głębszy → tor bliższy osi.
|
|
331
|
+
expect(by.get("g1")!.lane).toBeGreaterThan(0);
|
|
332
|
+
const f1 = by.get("f1")!;
|
|
333
|
+
const h1 = by.get("h1")!;
|
|
334
|
+
// Oba celują w p1 — kolejność stabilna, oba nad osią na różnych torach.
|
|
335
|
+
expect(f1.lane).toBeLessThan(0);
|
|
336
|
+
expect(h1.lane).toBeLessThan(0);
|
|
337
|
+
expect(f1.lane).not.toBe(h1.lane);
|
|
338
|
+
});
|
|
339
|
+
|
|
297
340
|
test("notatka dokleja się do poprzedzającego kroku", () => {
|
|
298
341
|
const occurrences: Occurrence[] = [
|
|
299
342
|
occ("m1", { view: "v" }, "m1"),
|
|
@@ -332,6 +375,57 @@ describe("buildProcessModel — fazy, Entry/Continue, notatki, konteksty", () =>
|
|
|
332
375
|
expect(m.steps[0].node?.contextPath).toEqual(["ctx_ai", "ctx_chat"]);
|
|
333
376
|
});
|
|
334
377
|
|
|
378
|
+
test("piny rozwiązują się z katalogu (catalogKind, label, pierwszeństwo MDX)", () => {
|
|
379
|
+
const g = toProcessGraph({
|
|
380
|
+
elements: [el("cmd", "command")],
|
|
381
|
+
edges: [],
|
|
382
|
+
catalog: [
|
|
383
|
+
{
|
|
384
|
+
kind: "personas",
|
|
385
|
+
name: "prospekt",
|
|
386
|
+
data: { name: "prospekt", label: "Prospekt", color: "#0e7490" },
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
kind: "metrics",
|
|
390
|
+
name: "leads.submitted",
|
|
391
|
+
data: { name: "leads.submitted", label: "Zgłoszone leady" },
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
});
|
|
395
|
+
const o: StepOccurrence = {
|
|
396
|
+
...occ("o1", { command: "cmd" }),
|
|
397
|
+
pins: [
|
|
398
|
+
{ kind: "persona", name: "prospekt" },
|
|
399
|
+
{ kind: "metric", name: "leads.submitted", label: "Leady (MDX)" },
|
|
400
|
+
{ kind: "persona", name: "nieznana" },
|
|
401
|
+
],
|
|
402
|
+
};
|
|
403
|
+
const m = buildProcessModel({ graph: g, profile: arcProfile, occurrences: [o] });
|
|
404
|
+
expect(m.steps[0].pins).toEqual([
|
|
405
|
+
{
|
|
406
|
+
kind: "persona",
|
|
407
|
+
name: "prospekt",
|
|
408
|
+
label: "Prospekt",
|
|
409
|
+
data: { name: "prospekt", label: "Prospekt", color: "#0e7490" },
|
|
410
|
+
resolved: true,
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
kind: "metric",
|
|
414
|
+
name: "leads.submitted",
|
|
415
|
+
label: "Leady (MDX)", // etykieta z MDX wygrywa nad katalogiem
|
|
416
|
+
data: { name: "leads.submitted", label: "Zgłoszone leady" },
|
|
417
|
+
resolved: true,
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
kind: "persona",
|
|
421
|
+
name: "nieznana",
|
|
422
|
+
label: "nieznana",
|
|
423
|
+
data: undefined,
|
|
424
|
+
resolved: false,
|
|
425
|
+
},
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
|
|
335
429
|
test("sides/expand z wystąpienia przechodzą na krok", () => {
|
|
336
430
|
const o: StepOccurrence = {
|
|
337
431
|
...occ("o1", { command: "cmd" }),
|
package/tests/layout.test.ts
CHANGED
|
@@ -134,6 +134,40 @@ describe("layoutProcess — tracki i kierunki", () => {
|
|
|
134
134
|
expect(edge).toBeDefined();
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
test("fork/join: blok Parallel dostaje rozwidlenie i scalenie", () => {
|
|
138
|
+
const occurrences: Occurrence[] = [
|
|
139
|
+
occ("s1", { command: "cmd" }),
|
|
140
|
+
{ ...occ("p1", { view: "v" }), path: [{ type: "parallel", id: "P", laneIndex: 0 }] },
|
|
141
|
+
{ ...occ("p2", { event: "e" }), path: [{ type: "parallel", id: "P", laneIndex: 1 }] },
|
|
142
|
+
occ("s2", { listener: "l" }),
|
|
143
|
+
];
|
|
144
|
+
const { layout } = run(occurrences);
|
|
145
|
+
const bb = layout.edges
|
|
146
|
+
.filter((e) => e.backbone)
|
|
147
|
+
.map((e) => `${e.source}->${e.target}`);
|
|
148
|
+
// Fork: s1 → oba lane'y; join: oba lane'y → s2.
|
|
149
|
+
expect(bb).toContain("s1->p1");
|
|
150
|
+
expect(bb).toContain("s1->p2");
|
|
151
|
+
expect(bb).toContain("p1->s2");
|
|
152
|
+
expect(bb).toContain("p2->s2");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("szkielet osi głównej nie przerywa się na bloku Entry w środku dokumentu", () => {
|
|
156
|
+
const occurrences: Occurrence[] = [
|
|
157
|
+
{ ...occ("m1", { command: "cmd" }), path: [{ type: "step", id: "sA", phaseId: "a" }] },
|
|
158
|
+
// Entry wtrącone w dokument między fazami.
|
|
159
|
+
{ ...occ("e1", { view: "v" }), path: [{ type: "entry", id: "en", title: "E" }] },
|
|
160
|
+
{ type: "continue", to: "b", path: [{ type: "entry", id: "en" }] },
|
|
161
|
+
{ ...occ("m2", { event: "e" }), path: [{ type: "step", id: "sB", phaseId: "b" }] },
|
|
162
|
+
];
|
|
163
|
+
const { layout } = run(occurrences);
|
|
164
|
+
const bb = layout.edges
|
|
165
|
+
.filter((e) => e.backbone)
|
|
166
|
+
.map((e) => `${e.source}->${e.target}`);
|
|
167
|
+
expect(bb).toContain("m1->m2"); // oś główna ciągła mimo Entry pomiędzy
|
|
168
|
+
expect(bb).not.toContain("e1->m2"); // Entry łączy się przez continues, nie szkielet
|
|
169
|
+
});
|
|
170
|
+
|
|
137
171
|
test("Entry na torze ujemnym renderuje się nad osią + krawędź continues", () => {
|
|
138
172
|
const occurrences: Occurrence[] = [
|
|
139
173
|
{
|