Skip to content

Commit f10d1f0

Browse files
committed
Merge branch 'sy-3833-strongly-type-schematics-v2' of https://github.com/synnaxlabs/synnax into sy-3833-2
2 parents 7949265 + d5ed762 commit f10d1f0

684 files changed

Lines changed: 26245 additions & 13790 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ work
7272
# |||| DATA ||||
7373

7474
**/data
75+
!pluto/src/schematic/edge/data
76+
!pluto/src/schematic/edge/data/**
7577
synnax-data
7678
synnax-data/**
7779

client/ts/src/arc/compiler/types.gen.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
// Code generated by Oracle. DO NOT EDIT.
1111

12+
import { record } from "@synnaxlabs/x";
1213
import { z } from "zod";
1314

1415
/**
@@ -22,6 +23,6 @@ export const outputZ = z.object({
2223
* outputMemoryBases contains memory base addresses for multi-output functions, mapping
2324
* function keys to their base addresses.
2425
*/
25-
outputMemoryBases: z.record(z.string(), z.uint32()),
26+
outputMemoryBases: record.nullishToEmpty(z.string(), z.uint32()),
2627
});
2728
export interface Output extends z.infer<typeof outputZ> {}

client/ts/src/arc/ir/types.gen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
// Code generated by Oracle. DO NOT EDIT.
1111

12-
import { array, zod } from "@synnaxlabs/x";
12+
import { array, record, zod } from "@synnaxlabs/x";
1313
import { z } from "zod";
1414

1515
import { types } from "@/arc/types";
@@ -76,7 +76,7 @@ export const authoritiesZ = z.object({
7676
/** default is the default authority for all write channels not explicitly listed. */
7777
default: zod.uint8.optional(),
7878
/** channels maps channel keys to their specific authority values. */
79-
channels: z.record(z.uint32(), zod.uint8),
79+
channels: record.nullishToEmpty(z.uint32(), zod.uint8),
8080
});
8181
export interface Authorities extends z.infer<typeof authoritiesZ> {}
8282

client/ts/src/arc/types/types.gen.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
// Code generated by Oracle. DO NOT EDIT.
1111

12-
import { array, zod } from "@synnaxlabs/x";
12+
import { array, record, zod } from "@synnaxlabs/x";
1313
import { z } from "zod";
1414

1515
export enum Kind {
@@ -48,9 +48,9 @@ export const chanDirectionZ = z.enum(ChanDirection);
4848
/** Channels contains channel declarations for reading from and writing to Synnax channels. */
4949
export const channelsZ = z.object({
5050
/** read contains readable channel indices mapped to parameter names. */
51-
read: z.record(z.uint32(), z.string()),
51+
read: record.nullishToEmpty(z.uint32(), z.string()),
5252
/** write contains writable channel indices mapped to parameter names. */
53-
write: z.record(z.uint32(), z.string()),
53+
write: record.nullishToEmpty(z.uint32(), z.string()),
5454
});
5555
export interface Channels extends z.infer<typeof channelsZ> {}
5656

client/ts/src/schematic/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,5 +179,5 @@ export const ZERO_NEW: New = {
179179
legend: ZERO_LEGEND,
180180
nodes: [],
181181
edges: [],
182-
props: {},
182+
configs: {},
183183
};

client/ts/src/schematic/schematic.spec.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ describe("Schematic", () => {
7373
...schematic.ZERO_NEW,
7474
authority: 5,
7575
nodes: [{ key: "n1", position: { x: 10, y: 20 }, zIndex: 0 }],
76-
props: { n1: { variant: "valve" } },
76+
configs: { n1: { variant: "valve" } },
7777
});
7878
const res = await client.schematics.retrieve({ key: schem.key });
7979
expect(res.name).toEqual("Schematic");
8080
expect(res.authority).toEqual(5);
8181
expect(res.nodes).toHaveLength(1);
8282
expect(res.nodes[0].key).toEqual("n1");
83-
expect(res.props.n1.variant).toEqual("valve");
83+
expect((res.configs.n1 as Record<string, unknown>).variant).toEqual("valve");
8484
});
8585
});
8686

@@ -101,12 +101,12 @@ describe("Schematic", () => {
101101
});
102102
});
103103

104-
describe("props case preservation", () => {
105-
test("preserves arbitrary key casing within prop values", async () => {
104+
describe("config case preservation", () => {
105+
test("preserves arbitrary key casing within config values", async () => {
106106
const ws = await client.workspaces.create({ name: "CaseTest", layout: {} });
107107
const schem = await client.schematics.create(ws.key, {
108108
...schematic.ZERO_NEW,
109-
props: {
109+
configs: {
110110
n1: {
111111
camelCaseKey: "value1",
112112
PascalCaseKey: "value2",
@@ -119,13 +119,19 @@ describe("Schematic", () => {
119119
},
120120
});
121121
const retrieved = await client.schematics.retrieve({ key: schem.key });
122-
const props = retrieved.props.n1;
123-
expect(props.camelCaseKey).toEqual("value1");
124-
expect(props.PascalCaseKey).toEqual("value2");
125-
expect(props.snake_case_key).toEqual("value3");
126-
const nested = props.nested as Record<string, unknown>;
127-
expect(nested.innerCamelCase).toEqual(123);
128-
expect((nested.InnerPascalCase as Record<string, unknown>).deepKey).toEqual(true);
122+
const config = retrieved.configs.n1 as Record<string, unknown>;
123+
expect(config.camelCaseKey).toEqual("value1");
124+
expect(config.PascalCaseKey).toEqual("value2");
125+
expect(config.snake_case_key).toEqual("value3");
126+
expect((config.nested as Record<string, unknown>).innerCamelCase).toEqual(123);
127+
expect(
128+
(
129+
(config.nested as Record<string, unknown>).InnerPascalCase as Record<
130+
string,
131+
unknown
132+
>
133+
).deepKey,
134+
).toEqual(true);
129135
});
130136
});
131137

client/ts/src/schematic/types.gen.ts

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,14 @@ import { z } from "zod";
1414

1515
import { ontology } from "@/ontology";
1616

17-
export const EDGE_VARIANTS = [
18-
"pipe",
19-
"electric",
20-
"secondary",
21-
"jacketed",
22-
"hydraulic",
23-
"pneumatic",
24-
"data",
25-
] as const;
26-
export const edgeVariantZ = z.enum(EDGE_VARIANTS);
27-
export type EdgeVariant = z.infer<typeof edgeVariantZ>;
28-
2917
/** Legend is the control legend overlay configuration. */
3018
export const legendZ = z.object({
3119
/** visible is whether the legend is visible. */
3220
visible: z.boolean(),
3321
/** position is the legend position within the schematic. */
3422
position: spatial.stickyXYZ,
3523
/** colors maps control status keys to their display colors. */
36-
colors: z.record(z.string(), color.colorZ),
24+
colors: record.nullishToEmpty(z.string(), color.colorZ),
3725
});
3826
export interface Legend extends z.infer<typeof legendZ> {}
3927

@@ -64,15 +52,6 @@ export const handleZ = z.object({
6452
});
6553
export interface Handle extends z.infer<typeof handleZ> {}
6654

67-
/** Segment is an orthogonal path segment with a direction and signed length. */
68-
export const segmentZ = z.object({
69-
/** direction is the axis of travel: x (horizontal) or y (vertical). */
70-
direction: spatial.directionZ,
71-
/** length is the signed distance along the axis. */
72-
length: z.number(),
73-
});
74-
export interface Segment extends z.infer<typeof segmentZ> {}
75-
7655
export const keyZ = z.uuid();
7756
export type Key = z.infer<typeof keyZ>;
7857

@@ -87,17 +66,6 @@ export const edgeZ = z.object({
8766
});
8867
export interface Edge extends z.infer<typeof edgeZ> {}
8968

90-
/** EdgeProps contains visual properties for an edge, stored in schematic props. */
91-
export const edgePropsZ = z.object({
92-
/** segments defines the orthogonal path segments from source to target. */
93-
segments: array.nullishToEmpty(segmentZ),
94-
/** variant is the visual style of the edge. */
95-
variant: edgeVariantZ.default("pipe"),
96-
/** color is the optional display color. */
97-
color: color.colorZ.optional(),
98-
});
99-
export interface EdgeProps extends z.infer<typeof edgePropsZ> {}
100-
10169
/**
10270
* Schematic is a visual diagram editor component for drawing system schematics,
10371
* control flows, and process diagrams. Schematics support interactive
@@ -119,10 +87,11 @@ export const schematicZ = z.object({
11987
/** edges contains all connections between nodes. */
12088
edges: array.nullishToEmpty(edgeZ),
12189
/**
122-
* props contains symbol-specific properties keyed by node or edge key,
123-
* including colors, labels, segments, and other visual configuration.
90+
* configs contains per-element configuration keyed by node or edge key. The
91+
* shape of each value is determined by the element's variant; the
92+
* wire format intentionally stores it as an opaque record.
12493
*/
125-
props: caseconv.preserveCase(z.record(z.string(), record.unknownZ())),
94+
configs: caseconv.preserveCase(record.nullishToEmpty(z.string(), record.unknownZ())),
12695
});
12796
export interface Schematic extends z.infer<typeof schematicZ> {}
12897

console/src/access/role/ontology.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// included in the file licenses/APL.txt.
99

1010
import { access } from "@synnaxlabs/client";
11-
import { Access, Icon, Menu } from "@synnaxlabs/pluto";
11+
import { Access, Icon, Menu, User } from "@synnaxlabs/pluto";
1212

1313
import { ContextMenu } from "@/components";
1414
import { Ontology } from "@/ontology";
@@ -68,10 +68,8 @@ export const ONTOLOGY_SERVICE: Ontology.Service = {
6868
icon: <Icon.Role />,
6969
TreeContextMenu,
7070
hasChildren: true,
71-
canDrop: ({ items }) =>
72-
items.every(
73-
({ key, type, data }) =>
74-
(key.toString().startsWith("user:") || type === "user") &&
75-
data?.rootUser !== true,
76-
),
71+
canDrop: ({ items }) => {
72+
const users = User.filterHaulItems(items);
73+
return users.length === items.length && users.every(({ data }) => !data.rootUser);
74+
},
7775
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2026 Synnax Labs, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included in the file
4+
// licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with the Business Source
7+
// License, use of this software will be governed by the Apache License, Version 2.0,
8+
// included in the file licenses/APL.txt.
9+
10+
import { type Haul } from "@synnaxlabs/pluto";
11+
import { describe, expect, it } from "vitest";
12+
13+
import {
14+
canDropHaulItem,
15+
createHaulItem,
16+
filterHaulItems,
17+
HAUL_TYPE,
18+
isHaulItem,
19+
} from "@/arc/editor/graph/Editor";
20+
21+
const KEY = "stage-key";
22+
const OTHER: Haul.Item = { type: "other_type", key: "other" };
23+
24+
describe("arc element haul utilities", () => {
25+
describe("createHaulItem", () => {
26+
it("creates an item with the arc element HAUL_TYPE", () => {
27+
expect(createHaulItem(KEY).type).toEqual(HAUL_TYPE);
28+
});
29+
30+
it("creates an item with the provided key", () => {
31+
expect(createHaulItem(KEY).key).toEqual(KEY);
32+
});
33+
});
34+
35+
describe("isHaulItem", () => {
36+
it("returns true for an arc element item", () => {
37+
expect(isHaulItem(createHaulItem(KEY))).toBe(true);
38+
});
39+
40+
it("returns false for an item of another kind", () => {
41+
expect(isHaulItem(OTHER)).toBe(false);
42+
});
43+
});
44+
45+
describe("filterHaulItems", () => {
46+
it("keeps arc element items and drops items of other kinds", () => {
47+
const item = createHaulItem(KEY);
48+
expect(filterHaulItems([item, OTHER])).toEqual([item]);
49+
});
50+
});
51+
52+
describe("canDropHaulItem", () => {
53+
it("returns true when at least one item is an arc element item", () => {
54+
expect(
55+
canDropHaulItem({ source: OTHER, items: [createHaulItem(KEY), OTHER] }),
56+
).toBe(true);
57+
});
58+
59+
it("returns false when no item is an arc element item", () => {
60+
expect(canDropHaulItem({ source: OTHER, items: [OTHER] })).toBe(false);
61+
});
62+
});
63+
});

console/src/arc/editor/graph/Editor.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,27 @@ import { useUndoableDispatch } from "@/hooks/useUndoableDispatch";
5353
import { Layout } from "@/layout";
5454
import { type RootState } from "@/store";
5555

56-
export const HAUL_TYPE = "arc-element";
56+
export const HAUL_TYPE = "arc_element";
5757

58-
const StageRenderer = ({
58+
export type HaulItem = Haul.Item<typeof HAUL_TYPE, string, undefined>;
59+
60+
export const createHaulItem = (key: string): HaulItem => ({ type: HAUL_TYPE, key });
61+
62+
export const isHaulItem = (item: Haul.Item): item is HaulItem =>
63+
item.type === HAUL_TYPE;
64+
65+
export const filterHaulItems = (items: Haul.Item[]): HaulItem[] =>
66+
items.filter(isHaulItem);
67+
68+
export const canDropHaulItem = Haul.canDropOfType<HaulItem>(HAUL_TYPE);
69+
70+
const NodeRenderer = ({
5971
nodeKey,
6072
position,
6173
selected,
6274
draggable,
6375
}: Diagram.NodeProps): ReactElement | null => {
64-
const { layoutKey, dispatch } = useArcEditorContext("ArcEditor.StageRenderer");
76+
const { layoutKey, dispatch } = useArcEditorContext("ArcEditor.NodeRenderer");
6577
const props = useSelectNodeProps(layoutKey, nodeKey);
6678
const { key = "", ...rest } = props ?? {};
6779
const handleChange = useCallback(
@@ -94,7 +106,7 @@ const StageRenderer = ({
94106
};
95107

96108
const ArcDiagram = Base.create({
97-
node: Component.renderProp(StageRenderer),
109+
node: Component.renderProp(NodeRenderer),
98110
});
99111

100112
export const ContextMenu: Layout.ContextMenuRenderer = ({ layoutKey }) => (
@@ -131,9 +143,15 @@ export const Editor: Layout.Renderer = ({ layoutKey, visible }) => {
131143
);
132144

133145
const handleNodesChange = useCallback(
134-
(changes: Diagram.NodeChange[]) =>
135-
undoableDispatch(applyNodeChanges({ key: layoutKey, changes })),
136-
[layoutKey, undoableDispatch],
146+
(changes: Diagram.NodeChange[]) => {
147+
const dragging = changes.some(
148+
(c) => c.type === "position" && c.dragging === true,
149+
);
150+
const action = applyNodeChanges({ key: layoutKey, changes });
151+
if (dragging) dispatch(action);
152+
else undoableDispatch(action);
153+
},
154+
[layoutKey, dispatch, undoableDispatch],
137155
);
138156

139157
const handleEdgesChange = useCallback(
@@ -172,9 +190,9 @@ export const Editor: Layout.Renderer = ({ layoutKey, visible }) => {
172190

173191
const handleDrop = useCallback(
174192
({ items, event }: Haul.OnDropProps): Haul.Item[] => {
175-
const valid = Haul.filterByType(HAUL_TYPE, items);
193+
const valid = filterHaulItems(items);
176194
if (ref.current == null || event == null) return valid;
177-
valid.forEach(({ key, data }) => {
195+
valid.forEach(({ key }) => {
178196
const spec = Base.Stage.REGISTRY[key];
179197
if (spec == null) return;
180198
const pos = xy.truncate(calculateCursorPosition(event), 0);
@@ -183,7 +201,7 @@ export const Editor: Layout.Renderer = ({ layoutKey, visible }) => {
183201
key: layoutKey,
184202
elKey: id.create(),
185203
node: { position: pos, zIndex: spec.zIndex },
186-
props: { key, ...spec.defaultProps(theme), ...(data ?? {}) },
204+
props: { key, ...spec.defaultProps(theme) },
187205
}),
188206
);
189207
});
@@ -195,7 +213,7 @@ export const Editor: Layout.Renderer = ({ layoutKey, visible }) => {
195213
const dropProps = Haul.useDrop({
196214
type: "arc",
197215
key: layoutKey,
198-
canDrop: Haul.canDropOfType(HAUL_TYPE),
216+
canDrop: canDropHaulItem,
199217
onDrop: handleDrop,
200218
});
201219

0 commit comments

Comments
 (0)