@burdenoff/fluidgrids-fe-sdk 2026.1102.2 → 2026.1102.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/dist/index.js CHANGED
@@ -68,12 +68,12 @@ export { ENGLISH_FALLBACK_TRANSLATOR, FluidGridsTranslationProvider, useTr } fro
68
68
  import { getFluidGridsClient, createFluidGridsClient } from './chunk-NTWG3CJX.js';
69
69
  export { createFluidGridsClient, getFluidGridsClient, getFluidGridsClientSafe, isFluidGridsClientReady, resetFluidGridsClient, setFluidGridsClient, waitForFluidGridsClient } from './chunk-NTWG3CJX.js';
70
70
  import * as React3 from 'react';
71
- import { lazy, Suspense, useState, useRef, useMemo, useCallback, useEffect } from 'react';
72
- import { Routes, Route, Navigate, useNavigate } from 'react-router';
71
+ import { lazy, memo, Suspense, useState, useRef, useMemo, useCallback, useEffect } from 'react';
72
+ import { Routes, Route, Navigate, useNavigate, useParams } from 'react-router';
73
73
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
74
74
  import { v4 } from 'uuid';
75
75
  import { toast } from 'sonner';
76
- import { ChevronRight, Check, Circle, Sparkles, Loader2, Square, Lightbulb, MessageCircleQuestion, RefreshCw, TriangleAlert, CheckCircle2, Pencil, Database, ArrowLeft, Plus, Table2, Search, ChevronDown, ChevronUp, Trash2, ArrowUpFromLine, ArrowDownToLine, ChevronLeft } from 'lucide-react';
76
+ import { ChevronRight, Check, Circle, Flag, Search, Calculator, ListTree, GitBranch, Play, Sparkles, Loader2, Square, Lightbulb, MessageCircleQuestion, RefreshCw, TriangleAlert, CheckCircle2, Pencil, Database, ArrowLeft, Plus, Table2, Scale, Trash2, ChevronDown, ChevronUp, ArrowUpFromLine, ArrowDownToLine, ChevronLeft, AlertTriangle, Save, FlaskConical, CircleSlash, XCircle } from 'lucide-react';
77
77
  import { Badge, Card, CardHeader, CardContent, Label, Textarea, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Button, Input, Tabs, TabsList, TabsTrigger, TabsContent, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, PagePurpose, NextSteps, CardTitle, IllustratedEmptyState, Switch, CardDescription } from '@burdenoff/fe-libs/ui';
78
78
  import { ErrorBoundary } from '@burdenoff/fe-libs/shared/components';
79
79
  import '@xyflow/react/dist/style.css';
@@ -81,6 +81,8 @@ import { Icon } from '@iconify/react';
81
81
  import { Button as Button$1 } from '@burdenoff/fe-libs/button';
82
82
  import { gql } from '@apollo/client';
83
83
  import { TagBadge } from '@burdenoff/fe-libs/tag';
84
+ import { MarkerType, Handle, Position, ReactFlowProvider, useReactFlow, useNodesState, useEdgesState, addEdge, ReactFlow, Background, Controls, MiniMap } from '@xyflow/react';
85
+ import { useTheme } from '@burdenoff/fe-libs/shared/hooks/shell';
84
86
  import * as TabsPrimitive from '@radix-ui/react-tabs';
85
87
  import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
86
88
  import * as SeparatorPrimitive from '@radix-ui/react-separator';
@@ -5021,6 +5023,2580 @@ function DataTablesPage() {
5021
5023
  ] })
5022
5024
  );
5023
5025
  }
5026
+ var ISSUE_FIELDS = gql`
5027
+ fragment RuleIssueFields on RuleIssue {
5028
+ severity
5029
+ code
5030
+ message
5031
+ nodeId
5032
+ }
5033
+ `;
5034
+ var RULE_SET_FIELDS = gql`
5035
+ ${ISSUE_FIELDS}
5036
+ fragment RuleSetFields on RuleSet {
5037
+ id
5038
+ workspaceId
5039
+ name
5040
+ description
5041
+ definition
5042
+ sampleInput
5043
+ nodeCount
5044
+ version
5045
+ issues {
5046
+ ...RuleIssueFields
5047
+ }
5048
+ createdAt
5049
+ updatedAt
5050
+ createdBy
5051
+ updatedBy
5052
+ }
5053
+ `;
5054
+ var GET_RULE_SETS = gql`
5055
+ ${RULE_SET_FIELDS}
5056
+ query GetRuleSets($search: String, $limit: Int, $offset: Int) {
5057
+ ruleSets(search: $search, limit: $limit, offset: $offset) {
5058
+ ruleSets {
5059
+ ...RuleSetFields
5060
+ }
5061
+ total
5062
+ }
5063
+ }
5064
+ `;
5065
+ var GET_RULE_SET = gql`
5066
+ ${RULE_SET_FIELDS}
5067
+ query GetRuleSet($id: ID!) {
5068
+ ruleSet(id: $id) {
5069
+ ...RuleSetFields
5070
+ }
5071
+ }
5072
+ `;
5073
+ var GET_RULE_SET_TEMPLATES = gql`
5074
+ query GetRuleSetTemplates {
5075
+ ruleSetTemplates {
5076
+ id
5077
+ name
5078
+ description
5079
+ definition
5080
+ sampleInput
5081
+ }
5082
+ }
5083
+ `;
5084
+ var CREATE_RULE_SET = gql`
5085
+ ${RULE_SET_FIELDS}
5086
+ mutation CreateRuleSet($input: CreateRuleSetInput!) {
5087
+ createRuleSet(input: $input) {
5088
+ ...RuleSetFields
5089
+ }
5090
+ }
5091
+ `;
5092
+ var UPDATE_RULE_SET = gql`
5093
+ ${RULE_SET_FIELDS}
5094
+ mutation UpdateRuleSet($id: ID!, $input: UpdateRuleSetInput!) {
5095
+ updateRuleSet(id: $id, input: $input) {
5096
+ ...RuleSetFields
5097
+ }
5098
+ }
5099
+ `;
5100
+ var DELETE_RULE_SET = gql`
5101
+ mutation DeleteRuleSet($id: ID!) {
5102
+ deleteRuleSet(id: $id)
5103
+ }
5104
+ `;
5105
+ var TEST_RULE_SET = gql`
5106
+ ${ISSUE_FIELDS}
5107
+ mutation TestRuleSet($input: TestRuleSetInput!) {
5108
+ testRuleSet(input: $input) {
5109
+ status
5110
+ decision
5111
+ output
5112
+ variables
5113
+ path
5114
+ durationMs
5115
+ issues {
5116
+ ...RuleIssueFields
5117
+ }
5118
+ trace {
5119
+ index
5120
+ nodeId
5121
+ nodeType
5122
+ label
5123
+ outcome
5124
+ detail
5125
+ variables
5126
+ conditions {
5127
+ field
5128
+ operator
5129
+ expected
5130
+ actual
5131
+ passed
5132
+ }
5133
+ }
5134
+ }
5135
+ }
5136
+ `;
5137
+ function operationError2(error, fallback) {
5138
+ return new Error(describeOperationError(error, fallback));
5139
+ }
5140
+ var RULE_SET_LIST_LIMIT = 200;
5141
+ function useRuleSets(search) {
5142
+ const [ruleSets, setRuleSets] = useState([]);
5143
+ const [total, setTotal] = useState(0);
5144
+ const [loading, setLoading] = useState(true);
5145
+ const [error, setError] = useState(null);
5146
+ const latestRequest = useRef(0);
5147
+ const fetchRuleSets = useCallback(async () => {
5148
+ latestRequest.current += 1;
5149
+ const request = latestRequest.current;
5150
+ setLoading(true);
5151
+ setError(null);
5152
+ try {
5153
+ const result = throwIfResultError(
5154
+ await getFluidGridsClient().query({
5155
+ query: GET_RULE_SETS,
5156
+ variables: {
5157
+ search: search?.trim() || void 0,
5158
+ limit: RULE_SET_LIST_LIMIT
5159
+ },
5160
+ fetchPolicy: "network-only"
5161
+ })
5162
+ );
5163
+ if (request !== latestRequest.current) return;
5164
+ setRuleSets(result.data?.ruleSets.ruleSets ?? []);
5165
+ setTotal(result.data?.ruleSets.total ?? 0);
5166
+ } catch (err) {
5167
+ if (request !== latestRequest.current) return;
5168
+ setError(operationError2(err, "Failed to load rule sets"));
5169
+ } finally {
5170
+ setLoading(
5171
+ (current) => request === latestRequest.current ? false : current
5172
+ );
5173
+ }
5174
+ }, [search]);
5175
+ useEffect(() => {
5176
+ void fetchRuleSets();
5177
+ }, [fetchRuleSets]);
5178
+ return { ruleSets, total, loading, error, refetch: fetchRuleSets };
5179
+ }
5180
+ function useRuleSet(id) {
5181
+ const [ruleSet, setRuleSet] = useState(null);
5182
+ const [loading, setLoading] = useState(Boolean(id));
5183
+ const [error, setError] = useState(null);
5184
+ const fetchRuleSet = useCallback(async () => {
5185
+ if (!id) return;
5186
+ setLoading(true);
5187
+ setError(null);
5188
+ try {
5189
+ const result = throwIfResultError(
5190
+ await getFluidGridsClient().query({
5191
+ query: GET_RULE_SET,
5192
+ variables: { id },
5193
+ fetchPolicy: "network-only"
5194
+ })
5195
+ );
5196
+ setRuleSet(result.data?.ruleSet ?? null);
5197
+ } catch (err) {
5198
+ setError(operationError2(err, "Failed to load the rule set"));
5199
+ } finally {
5200
+ setLoading(false);
5201
+ }
5202
+ }, [id]);
5203
+ useEffect(() => {
5204
+ void fetchRuleSet();
5205
+ }, [fetchRuleSet]);
5206
+ return { ruleSet, setRuleSet, loading, error, refetch: fetchRuleSet };
5207
+ }
5208
+ function useRuleSetTemplates() {
5209
+ const [templates, setTemplates] = useState([]);
5210
+ const [loading, setLoading] = useState(true);
5211
+ useEffect(() => {
5212
+ let active = true;
5213
+ getFluidGridsClient().query({
5214
+ query: GET_RULE_SET_TEMPLATES,
5215
+ fetchPolicy: "cache-first"
5216
+ }).then((result) => {
5217
+ if (active) setTemplates(result.data?.ruleSetTemplates ?? []);
5218
+ }).catch(() => {
5219
+ if (active) setTemplates([]);
5220
+ }).finally(() => {
5221
+ if (active) setLoading(false);
5222
+ });
5223
+ return () => {
5224
+ active = false;
5225
+ };
5226
+ }, []);
5227
+ return { templates, loading };
5228
+ }
5229
+ function useRuleSetMutations() {
5230
+ const createRuleSet = useCallback(async (input) => {
5231
+ try {
5232
+ const result = throwIfResultError(
5233
+ await getFluidGridsClient().mutate({
5234
+ mutation: CREATE_RULE_SET,
5235
+ variables: { input }
5236
+ })
5237
+ );
5238
+ const created = result.data?.createRuleSet;
5239
+ if (!created) throw new Error("Failed to create the rule set");
5240
+ return created;
5241
+ } catch (err) {
5242
+ throw operationError2(err, "Failed to create the rule set");
5243
+ }
5244
+ }, []);
5245
+ const updateRuleSet = useCallback(
5246
+ async (id, input) => {
5247
+ try {
5248
+ const result = throwIfResultError(
5249
+ await getFluidGridsClient().mutate({
5250
+ mutation: UPDATE_RULE_SET,
5251
+ variables: { id, input }
5252
+ })
5253
+ );
5254
+ const updated = result.data?.updateRuleSet;
5255
+ if (!updated) throw new Error("Failed to save the rule set");
5256
+ return updated;
5257
+ } catch (err) {
5258
+ throw operationError2(err, "Failed to save the rule set");
5259
+ }
5260
+ },
5261
+ []
5262
+ );
5263
+ const deleteRuleSet = useCallback(async (id) => {
5264
+ try {
5265
+ const result = throwIfResultError(
5266
+ await getFluidGridsClient().mutate({
5267
+ mutation: DELETE_RULE_SET,
5268
+ variables: { id }
5269
+ })
5270
+ );
5271
+ return result.data?.deleteRuleSet === true;
5272
+ } catch (err) {
5273
+ throw operationError2(err, "Failed to delete the rule set");
5274
+ }
5275
+ }, []);
5276
+ const testRuleSet = useCallback(
5277
+ async (input) => {
5278
+ try {
5279
+ const result = throwIfResultError(
5280
+ await getFluidGridsClient().mutate({
5281
+ mutation: TEST_RULE_SET,
5282
+ variables: { input }
5283
+ })
5284
+ );
5285
+ const evaluation = result.data?.testRuleSet;
5286
+ if (!evaluation) throw new Error("Failed to test the rule set");
5287
+ return evaluation;
5288
+ } catch (err) {
5289
+ throw operationError2(err, "Failed to test the rule set");
5290
+ }
5291
+ },
5292
+ []
5293
+ );
5294
+ return { createRuleSet, updateRuleSet, deleteRuleSet, testRuleSet };
5295
+ }
5296
+ function CreateRuleSetDialog({
5297
+ open,
5298
+ onOpenChange,
5299
+ onCreated
5300
+ }) {
5301
+ const tr = useTr();
5302
+ const { templates } = useRuleSetTemplates();
5303
+ const { createRuleSet } = useRuleSetMutations();
5304
+ const [name, setName] = useState("");
5305
+ const [description, setDescription] = useState("");
5306
+ const [templateId, setTemplateId] = useState("loan-approval");
5307
+ const [saving, setSaving] = useState(false);
5308
+ const submit = async (event) => {
5309
+ event.preventDefault();
5310
+ const template = templates.find((item) => item.id === templateId);
5311
+ const finalName = name.trim() || template?.name || "";
5312
+ if (!finalName) {
5313
+ toast.error(
5314
+ tr("fluidgrids.rules.nameRequired", "Give the rule set a name")
5315
+ );
5316
+ return;
5317
+ }
5318
+ setSaving(true);
5319
+ try {
5320
+ const created = await createRuleSet({
5321
+ name: finalName,
5322
+ description: description.trim() || template?.description || void 0,
5323
+ ...templateId ? { templateId } : {}
5324
+ });
5325
+ onCreated(created);
5326
+ setName("");
5327
+ setDescription("");
5328
+ } catch (err) {
5329
+ toast.error(
5330
+ err instanceof Error ? err.message : tr(
5331
+ "fluidgrids.rules.createFailed",
5332
+ "Failed to create the rule set"
5333
+ )
5334
+ );
5335
+ } finally {
5336
+ setSaving(false);
5337
+ }
5338
+ };
5339
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-lg", children: [
5340
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: tr("fluidgrids.rules.newRuleSet", "New rule set") }) }),
5341
+ /* @__PURE__ */ jsxs(
5342
+ "form",
5343
+ {
5344
+ className: "flex flex-col gap-4",
5345
+ onSubmit: (event) => void submit(event),
5346
+ children: [
5347
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
5348
+ /* @__PURE__ */ jsx(Label, { children: tr("fluidgrids.rules.startFrom", "Start from") }),
5349
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
5350
+ templates.map((template) => /* @__PURE__ */ jsxs(
5351
+ "label",
5352
+ {
5353
+ className: `flex cursor-pointer gap-3 rounded-md border p-3 ${templateId === template.id ? "border-primary bg-primary/5" : "border-border-secondary"}`,
5354
+ children: [
5355
+ /* @__PURE__ */ jsx(
5356
+ "input",
5357
+ {
5358
+ type: "radio",
5359
+ name: "rule-template",
5360
+ value: template.id,
5361
+ checked: templateId === template.id,
5362
+ onChange: () => setTemplateId(template.id),
5363
+ className: "mt-1"
5364
+ }
5365
+ ),
5366
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col", children: [
5367
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-text-primary", children: template.name }),
5368
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-text-secondary", children: template.description })
5369
+ ] })
5370
+ ]
5371
+ },
5372
+ template.id
5373
+ )),
5374
+ /* @__PURE__ */ jsxs(
5375
+ "label",
5376
+ {
5377
+ className: `flex cursor-pointer gap-3 rounded-md border p-3 ${templateId === "" ? "border-primary bg-primary/5" : "border-border-secondary"}`,
5378
+ children: [
5379
+ /* @__PURE__ */ jsx(
5380
+ "input",
5381
+ {
5382
+ type: "radio",
5383
+ name: "rule-template",
5384
+ value: "",
5385
+ checked: templateId === "",
5386
+ onChange: () => setTemplateId(""),
5387
+ className: "mt-1"
5388
+ }
5389
+ ),
5390
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col", children: [
5391
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-text-primary", children: tr("fluidgrids.rules.emptyRuleSet", "Empty canvas") }),
5392
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-text-secondary", children: tr(
5393
+ "fluidgrids.rules.emptyRuleSetHint",
5394
+ "No blocks; build the decision tree from scratch."
5395
+ ) })
5396
+ ] })
5397
+ ]
5398
+ }
5399
+ )
5400
+ ] })
5401
+ ] }),
5402
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
5403
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-set-name", children: tr("fluidgrids.rules.name", "Name") }),
5404
+ /* @__PURE__ */ jsx(
5405
+ Input,
5406
+ {
5407
+ id: "rule-set-name",
5408
+ value: name,
5409
+ maxLength: 200,
5410
+ placeholder: templates.find((item) => item.id === templateId)?.name ?? tr("fluidgrids.rules.namePlaceholder", "Loan approval decision"),
5411
+ onChange: (event) => setName(event.target.value)
5412
+ }
5413
+ )
5414
+ ] }),
5415
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
5416
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-set-description", children: tr("fluidgrids.rules.description", "Description") }),
5417
+ /* @__PURE__ */ jsx(
5418
+ Textarea,
5419
+ {
5420
+ id: "rule-set-description",
5421
+ rows: 2,
5422
+ maxLength: 2e3,
5423
+ value: description,
5424
+ onChange: (event) => setDescription(event.target.value)
5425
+ }
5426
+ )
5427
+ ] }),
5428
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
5429
+ /* @__PURE__ */ jsx(
5430
+ Button$1,
5431
+ {
5432
+ type: "button",
5433
+ variant: "outline",
5434
+ onClick: () => onOpenChange(false),
5435
+ children: tr("fluidgrids.rules.cancel", "Cancel")
5436
+ }
5437
+ ),
5438
+ /* @__PURE__ */ jsx(Button$1, { type: "submit", disabled: saving, children: saving ? tr("fluidgrids.rules.creating", "Creating\u2026") : tr("fluidgrids.rules.create", "Create and open") })
5439
+ ] })
5440
+ ]
5441
+ }
5442
+ )
5443
+ ] }) });
5444
+ }
5445
+ function RuleSetsPage() {
5446
+ const tr = useTr();
5447
+ const navigate = useNavigate();
5448
+ const [searchInput, setSearchInput] = useState("");
5449
+ const [search, setSearch] = useState("");
5450
+ const { ruleSets, total, loading, error, refetch } = useRuleSets(search);
5451
+ const { deleteRuleSet } = useRuleSetMutations();
5452
+ const [createOpen, setCreateOpen] = useState(false);
5453
+ const open = (ruleSet) => navigate(ruleSet.id);
5454
+ const remove = async (ruleSet) => {
5455
+ if (!window.confirm(
5456
+ tr(
5457
+ "fluidgrids.rules.confirmDelete",
5458
+ 'Delete the rule set "{{name}}"?',
5459
+ {
5460
+ name: ruleSet.name
5461
+ }
5462
+ )
5463
+ )) {
5464
+ return;
5465
+ }
5466
+ try {
5467
+ await deleteRuleSet(ruleSet.id);
5468
+ toast.success(tr("fluidgrids.rules.deleted", "Rule set deleted"));
5469
+ void refetch();
5470
+ } catch (err) {
5471
+ toast.error(
5472
+ err instanceof Error ? err.message : tr(
5473
+ "fluidgrids.rules.deleteFailed",
5474
+ "Failed to delete the rule set"
5475
+ )
5476
+ );
5477
+ }
5478
+ };
5479
+ const newButton = /* @__PURE__ */ jsx(
5480
+ SdkPermissionButton,
5481
+ {
5482
+ permission: FLUIDGRIDS_PERMISSIONS.WORKFLOW_CREATE,
5483
+ deniedTooltip: tr(
5484
+ "fluidgrids.rules.createDenied",
5485
+ "You don't have permission to create rule sets"
5486
+ ),
5487
+ children: /* @__PURE__ */ jsxs(Button$1, { onClick: () => setCreateOpen(true), "data-testid": "rule-set-new", children: [
5488
+ /* @__PURE__ */ jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }),
5489
+ tr("fluidgrids.rules.newRuleSet", "New rule set")
5490
+ ] })
5491
+ }
5492
+ );
5493
+ return /* @__PURE__ */ jsxs("div", { className: "min-h-screen bg-background-primary p-4 pb-24 sm:p-6 sm:pb-24", children: [
5494
+ /* @__PURE__ */ jsxs("div", { className: "mx-auto flex max-w-[1200px] flex-col gap-5", children: [
5495
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between", children: [
5496
+ /* @__PURE__ */ jsxs("div", { children: [
5497
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-2 text-sm text-text-secondary", children: [
5498
+ /* @__PURE__ */ jsx(Scale, { className: "size-4", "aria-hidden": "true" }),
5499
+ tr("fluidgrids.rules.eyebrow", "Decision logic")
5500
+ ] }),
5501
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-text-primary sm:text-3xl", children: tr("fluidgrids.rules.pageTitle", "Rule Builder") }),
5502
+ /* @__PURE__ */ jsx(PagePurpose, { icon: /* @__PURE__ */ jsx(Scale, { className: "size-4", "aria-hidden": "true" }), children: tr(
5503
+ "fluidgrids.rules.pageSubtitle",
5504
+ "Author decision trees visually from conditions, scoring rules, lookups and decisions, then test them against sample input and see every step of the evaluation."
5505
+ ) })
5506
+ ] }),
5507
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
5508
+ /* @__PURE__ */ jsxs(Button$1, { variant: "outline", onClick: () => navigate("../list"), children: [
5509
+ /* @__PURE__ */ jsx(ArrowLeft, { className: "mr-2 size-4", "aria-hidden": "true" }),
5510
+ tr("fluidgrids.rules.backToWorkflows", "Back to Workflows")
5511
+ ] }),
5512
+ newButton
5513
+ ] })
5514
+ ] }),
5515
+ /* @__PURE__ */ jsxs(
5516
+ "form",
5517
+ {
5518
+ className: "flex max-w-md gap-2",
5519
+ onSubmit: (event) => {
5520
+ event.preventDefault();
5521
+ setSearch(searchInput);
5522
+ },
5523
+ children: [
5524
+ /* @__PURE__ */ jsxs("div", { className: "relative min-w-0 flex-1", children: [
5525
+ /* @__PURE__ */ jsx(
5526
+ Search,
5527
+ {
5528
+ className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-text-secondary",
5529
+ "aria-hidden": "true"
5530
+ }
5531
+ ),
5532
+ /* @__PURE__ */ jsx(
5533
+ Input,
5534
+ {
5535
+ className: "pl-8",
5536
+ value: searchInput,
5537
+ onChange: (event) => setSearchInput(event.target.value),
5538
+ placeholder: tr("fluidgrids.rules.search", "Search rule sets"),
5539
+ "aria-label": tr("fluidgrids.rules.search", "Search rule sets")
5540
+ }
5541
+ )
5542
+ ] }),
5543
+ /* @__PURE__ */ jsx(Button$1, { type: "submit", variant: "outline", children: tr("fluidgrids.rules.searchAction", "Search") })
5544
+ ]
5545
+ }
5546
+ ),
5547
+ error && /* @__PURE__ */ jsx("p", { className: "text-sm text-status-error", role: "alert", children: error.message }),
5548
+ loading && ruleSets.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: tr("fluidgrids.rules.loading", "Loading rule sets\u2026") }),
5549
+ !loading && !error && ruleSets.length === 0 && /* @__PURE__ */ jsx(
5550
+ IllustratedEmptyState,
5551
+ {
5552
+ className: "py-10",
5553
+ illustration: "onboarding",
5554
+ title: search ? tr(
5555
+ "fluidgrids.rules.noMatches",
5556
+ "No rule sets match your search"
5557
+ ) : tr("fluidgrids.rules.empty", "Build your first rule set"),
5558
+ description: tr(
5559
+ "fluidgrids.rules.emptyHint",
5560
+ "Start from the loan approval example or an empty canvas."
5561
+ ),
5562
+ action: search ? void 0 : newButton
5563
+ }
5564
+ ),
5565
+ ruleSets.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
5566
+ /* @__PURE__ */ jsx("div", { className: "text-sm text-text-secondary", children: tr("fluidgrids.rules.count", "{{count}} rule sets", {
5567
+ count: total
5568
+ }) }),
5569
+ /* @__PURE__ */ jsx("div", { className: "grid gap-3 md:grid-cols-2", children: ruleSets.map((ruleSet) => {
5570
+ const errors = ruleSet.issues.filter(
5571
+ (issue) => issue.severity === "ERROR"
5572
+ ).length;
5573
+ return /* @__PURE__ */ jsx(
5574
+ Card,
5575
+ {
5576
+ className: "border-border-secondary transition-colors hover:border-primary/50",
5577
+ "data-testid": "rule-set-card",
5578
+ children: /* @__PURE__ */ jsxs(CardContent, { className: "flex items-start gap-3 p-4", children: [
5579
+ /* @__PURE__ */ jsxs(
5580
+ "button",
5581
+ {
5582
+ type: "button",
5583
+ className: "flex min-w-0 flex-1 flex-col text-left",
5584
+ onClick: () => open(ruleSet),
5585
+ children: [
5586
+ /* @__PURE__ */ jsx("span", { className: "truncate font-semibold text-text-primary", children: ruleSet.name }),
5587
+ ruleSet.description && /* @__PURE__ */ jsx("span", { className: "line-clamp-2 text-sm text-text-secondary", children: ruleSet.description }),
5588
+ /* @__PURE__ */ jsxs("span", { className: "mt-2 flex flex-wrap items-center gap-2 text-xs text-text-secondary", children: [
5589
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: tr(
5590
+ "fluidgrids.rules.blockCount",
5591
+ "{{count}} blocks",
5592
+ {
5593
+ count: ruleSet.nodeCount
5594
+ }
5595
+ ) }),
5596
+ /* @__PURE__ */ jsxs("span", { children: [
5597
+ "v",
5598
+ ruleSet.version
5599
+ ] }),
5600
+ /* @__PURE__ */ jsx("span", { children: tr(
5601
+ "fluidgrids.rules.updatedAt",
5602
+ "Updated {{date}}",
5603
+ {
5604
+ date: new Date(
5605
+ ruleSet.updatedAt
5606
+ ).toLocaleString()
5607
+ }
5608
+ ) }),
5609
+ errors > 0 && /* @__PURE__ */ jsx("span", { className: "text-status-error", children: tr(
5610
+ "fluidgrids.rules.needsAttention",
5611
+ "Needs attention"
5612
+ ) })
5613
+ ] })
5614
+ ]
5615
+ }
5616
+ ),
5617
+ /* @__PURE__ */ jsx(
5618
+ SdkPermissionButton,
5619
+ {
5620
+ permission: FLUIDGRIDS_PERMISSIONS.WORKFLOW_DELETE,
5621
+ deniedTooltip: tr(
5622
+ "fluidgrids.rules.deleteDenied",
5623
+ "You don't have permission to delete rule sets"
5624
+ ),
5625
+ children: /* @__PURE__ */ jsx(
5626
+ Button$1,
5627
+ {
5628
+ variant: "ghost",
5629
+ size: "sm",
5630
+ "aria-label": tr("fluidgrids.rules.delete", "Delete"),
5631
+ onClick: () => void remove(ruleSet),
5632
+ children: /* @__PURE__ */ jsx(Trash2, { className: "size-4", "aria-hidden": "true" })
5633
+ }
5634
+ )
5635
+ }
5636
+ )
5637
+ ] })
5638
+ },
5639
+ ruleSet.id
5640
+ );
5641
+ }) })
5642
+ ] })
5643
+ ] }),
5644
+ /* @__PURE__ */ jsx(
5645
+ CreateRuleSetDialog,
5646
+ {
5647
+ open: createOpen,
5648
+ onOpenChange: setCreateOpen,
5649
+ onCreated: (ruleSet) => {
5650
+ setCreateOpen(false);
5651
+ toast.success(tr("fluidgrids.rules.created", "Rule set created"));
5652
+ open(ruleSet);
5653
+ }
5654
+ }
5655
+ )
5656
+ ] });
5657
+ }
5658
+ var RULE_BLOCK_STYLES = {
5659
+ CONDITION: {
5660
+ icon: GitBranch,
5661
+ accent: "border-status-info",
5662
+ chip: "bg-status-info/10 text-status-info"
5663
+ },
5664
+ COMPLEX: {
5665
+ icon: ListTree,
5666
+ accent: "border-primary",
5667
+ chip: "bg-primary/10 text-primary"
5668
+ },
5669
+ SCORING: {
5670
+ icon: Calculator,
5671
+ accent: "border-status-warning",
5672
+ chip: "bg-status-warning/10 text-status-warning"
5673
+ },
5674
+ LOOKUP: {
5675
+ icon: Search,
5676
+ accent: "border-accent",
5677
+ chip: "bg-accent/10 text-accent"
5678
+ },
5679
+ DECISION: {
5680
+ icon: Flag,
5681
+ accent: "border-status-success",
5682
+ chip: "bg-status-success/10 text-status-success"
5683
+ }
5684
+ };
5685
+ var RULE_BLOCK_DRAG_MIME = "application/x-fluidgrids-rule-block";
5686
+ function ruleIssueKey(issue) {
5687
+ return `${issue.severity}:${issue.code}:${issue.nodeId ?? ""}:${issue.message}`;
5688
+ }
5689
+
5690
+ // src/modules/rule-builder/lib/rule-graph.ts
5691
+ var RULE_NODE_TYPES = [
5692
+ "CONDITION",
5693
+ "COMPLEX",
5694
+ "SCORING",
5695
+ "LOOKUP",
5696
+ "DECISION"
5697
+ ];
5698
+ var RULE_OPERATORS = [
5699
+ "EQ",
5700
+ "NEQ",
5701
+ "GT",
5702
+ "GTE",
5703
+ "LT",
5704
+ "LTE",
5705
+ "BETWEEN",
5706
+ "IN",
5707
+ "NOT_IN",
5708
+ "CONTAINS",
5709
+ "NOT_CONTAINS",
5710
+ "STARTS_WITH",
5711
+ "ENDS_WITH",
5712
+ "EXISTS",
5713
+ "NOT_EXISTS",
5714
+ "IS_TRUE",
5715
+ "IS_FALSE"
5716
+ ];
5717
+ var UNARY_OPERATORS = /* @__PURE__ */ new Set([
5718
+ "EXISTS",
5719
+ "NOT_EXISTS",
5720
+ "IS_TRUE",
5721
+ "IS_FALSE"
5722
+ ]);
5723
+ function configOf(_type, config2) {
5724
+ return config2;
5725
+ }
5726
+ function isPlainObject(value) {
5727
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5728
+ }
5729
+ function isBranching(type) {
5730
+ return type === "CONDITION" || type === "COMPLEX";
5731
+ }
5732
+ function branchesOf(type) {
5733
+ if (type === "DECISION") return [];
5734
+ return isBranching(type) ? ["TRUE", "FALSE"] : ["NEXT"];
5735
+ }
5736
+ function defaultConfig(type) {
5737
+ switch (type) {
5738
+ case "CONDITION":
5739
+ return { condition: { field: "", operator: "EQ", value: "" } };
5740
+ case "COMPLEX":
5741
+ return {
5742
+ combinator: "ALL",
5743
+ conditions: [{ field: "", operator: "EQ", value: "" }]
5744
+ };
5745
+ case "SCORING":
5746
+ return { variable: "score", base: 0, rules: [] };
5747
+ case "LOOKUP":
5748
+ return {
5749
+ key: "",
5750
+ variable: "result",
5751
+ source: { kind: "INLINE", entries: [] },
5752
+ defaultValue: null
5753
+ };
5754
+ case "DECISION":
5755
+ return { outcome: "APPROVED", output: {} };
5756
+ }
5757
+ }
5758
+ var DEFAULT_LABELS = {
5759
+ CONDITION: "Condition",
5760
+ COMPLEX: "Complex logic",
5761
+ SCORING: "Scoring rule",
5762
+ LOOKUP: "Lookup",
5763
+ DECISION: "Decision"
5764
+ };
5765
+ function nextNodeId(type, existing) {
5766
+ const taken = new Set(existing);
5767
+ const prefix = type.toLowerCase();
5768
+ for (let index = 1; ; index++) {
5769
+ const id = `${prefix}-${index}`;
5770
+ if (!taken.has(id)) return id;
5771
+ }
5772
+ }
5773
+ function createRuleNode(type, position, existingIds, label = DEFAULT_LABELS[type]) {
5774
+ return {
5775
+ id: nextNodeId(type, existingIds),
5776
+ type,
5777
+ label,
5778
+ position,
5779
+ config: defaultConfig(type)
5780
+ };
5781
+ }
5782
+ function readNode(value) {
5783
+ if (!isPlainObject(value)) return null;
5784
+ const type = value["type"];
5785
+ const id = value["id"];
5786
+ if (typeof id !== "string" || typeof type !== "string" || !RULE_NODE_TYPES.includes(type)) {
5787
+ return null;
5788
+ }
5789
+ const ruleType = type;
5790
+ const position = isPlainObject(value["position"]) ? value["position"] : {};
5791
+ const x = Number(position["x"]);
5792
+ const y = Number(position["y"]);
5793
+ const config2 = isPlainObject(value["config"]) ? {
5794
+ ...defaultConfig(ruleType),
5795
+ ...value["config"]
5796
+ } : defaultConfig(ruleType);
5797
+ return {
5798
+ id,
5799
+ type: ruleType,
5800
+ label: typeof value["label"] === "string" ? value["label"] : "",
5801
+ position: { x: Number.isFinite(x) ? x : 0, y: Number.isFinite(y) ? y : 0 },
5802
+ config: config2
5803
+ };
5804
+ }
5805
+ function readDefinition(value) {
5806
+ if (!isPlainObject(value)) return { startNodeId: null, nodes: [], edges: [] };
5807
+ const nodes = (Array.isArray(value["nodes"]) ? value["nodes"] : []).map(readNode).filter((node) => node !== null);
5808
+ const ids = new Set(nodes.map((node) => node.id));
5809
+ const edges = (Array.isArray(value["edges"]) ? value["edges"] : []).filter(isPlainObject).map((edge, index) => ({
5810
+ id: typeof edge["id"] === "string" ? edge["id"] : `edge-${index + 1}`,
5811
+ source: String(edge["source"] ?? ""),
5812
+ target: String(edge["target"] ?? ""),
5813
+ branch: branchFromHandle(
5814
+ typeof edge["branch"] === "string" ? edge["branch"] : null
5815
+ )
5816
+ })).filter((edge) => ids.has(edge.source) && ids.has(edge.target));
5817
+ const start = value["startNodeId"];
5818
+ return {
5819
+ startNodeId: typeof start === "string" && ids.has(start) ? start : null,
5820
+ nodes,
5821
+ edges
5822
+ };
5823
+ }
5824
+ function resolveStartNodeId(definition) {
5825
+ if (definition.startNodeId) return definition.startNodeId;
5826
+ const targets = new Set(definition.edges.map((edge) => edge.target));
5827
+ const roots = definition.nodes.filter((node) => !targets.has(node.id));
5828
+ return roots.length === 1 ? roots[0]?.id ?? null : null;
5829
+ }
5830
+ function toFlow(definition) {
5831
+ const start = resolveStartNodeId(definition);
5832
+ return {
5833
+ nodes: definition.nodes.map((node) => ({
5834
+ id: node.id,
5835
+ type: "rule",
5836
+ position: node.position,
5837
+ data: {
5838
+ ruleType: node.type,
5839
+ label: node.label,
5840
+ config: node.config,
5841
+ isStart: node.id === start
5842
+ }
5843
+ })),
5844
+ edges: definition.edges.map((edge) => toFlowEdge(edge))
5845
+ };
5846
+ }
5847
+ function toFlowEdge(edge) {
5848
+ return {
5849
+ id: edge.id,
5850
+ source: edge.source,
5851
+ target: edge.target,
5852
+ sourceHandle: edge.branch,
5853
+ label: edge.branch === "NEXT" ? void 0 : edge.branch,
5854
+ data: { branch: edge.branch }
5855
+ };
5856
+ }
5857
+ function fromFlow(nodes, edges, startNodeId) {
5858
+ const ids = new Set(nodes.map((node) => node.id));
5859
+ return {
5860
+ startNodeId: startNodeId && ids.has(startNodeId) ? startNodeId : null,
5861
+ nodes: nodes.map((node) => ({
5862
+ id: node.id,
5863
+ type: node.data.ruleType,
5864
+ label: node.data.label,
5865
+ position: {
5866
+ x: Math.round(node.position.x),
5867
+ y: Math.round(node.position.y)
5868
+ },
5869
+ config: node.data.config
5870
+ })),
5871
+ edges: edges.filter((edge) => ids.has(edge.source) && ids.has(edge.target)).map((edge) => ({
5872
+ id: edge.id,
5873
+ source: edge.source,
5874
+ target: edge.target,
5875
+ branch: edge.data?.branch ?? branchFromHandle(edge.sourceHandle)
5876
+ }))
5877
+ };
5878
+ }
5879
+ function branchFromHandle(handle) {
5880
+ return handle === "TRUE" || handle === "FALSE" ? handle : "NEXT";
5881
+ }
5882
+ function canConnect(edges, source, target, handle) {
5883
+ if (!source || !target || source === target) return false;
5884
+ const branch = branchFromHandle(handle);
5885
+ return !edges.some(
5886
+ (edge) => edge.source === source && (edge.data?.branch ?? branchFromHandle(edge.sourceHandle)) === branch
5887
+ );
5888
+ }
5889
+ function nextEdgeId(edges) {
5890
+ const taken = new Set(edges.map((edge) => edge.id));
5891
+ for (let index = edges.length + 1; ; index++) {
5892
+ const id = `edge-${index}`;
5893
+ if (!taken.has(id)) return id;
5894
+ }
5895
+ }
5896
+ function parseValueText(text) {
5897
+ const trimmed = text.trim();
5898
+ if (trimmed === "") return "";
5899
+ try {
5900
+ return JSON.parse(trimmed);
5901
+ } catch {
5902
+ return text;
5903
+ }
5904
+ }
5905
+ function formatValueText(value) {
5906
+ if (value === void 0 || value === null) return "";
5907
+ if (typeof value === "string") {
5908
+ return parseValueText(value) === value ? value : JSON.stringify(value);
5909
+ }
5910
+ return JSON.stringify(value);
5911
+ }
5912
+ function parseJsonText(text) {
5913
+ try {
5914
+ return { ok: true, value: JSON.parse(text) };
5915
+ } catch (error) {
5916
+ return {
5917
+ ok: false,
5918
+ error: error instanceof Error ? error.message : "Invalid JSON"
5919
+ };
5920
+ }
5921
+ }
5922
+
5923
+ // src/modules/rule-builder/lib/rule-labels.ts
5924
+ function ruleBlockTypeLabel(tr, type) {
5925
+ switch (type) {
5926
+ case "CONDITION":
5927
+ return tr("fluidgrids.rules.blockType.CONDITION", "Condition");
5928
+ case "COMPLEX":
5929
+ return tr("fluidgrids.rules.blockType.COMPLEX", "Complex logic");
5930
+ case "SCORING":
5931
+ return tr("fluidgrids.rules.blockType.SCORING", "Scoring rule");
5932
+ case "LOOKUP":
5933
+ return tr("fluidgrids.rules.blockType.LOOKUP", "Lookup");
5934
+ case "DECISION":
5935
+ return tr("fluidgrids.rules.blockType.DECISION", "Decision");
5936
+ }
5937
+ }
5938
+ function operatorLabel(tr, operator) {
5939
+ switch (operator) {
5940
+ case "EQ":
5941
+ return "=";
5942
+ case "NEQ":
5943
+ return "\u2260";
5944
+ case "GT":
5945
+ return ">";
5946
+ case "GTE":
5947
+ return "\u2265";
5948
+ case "LT":
5949
+ return "<";
5950
+ case "LTE":
5951
+ return "\u2264";
5952
+ case "BETWEEN":
5953
+ return tr("fluidgrids.rules.operator.BETWEEN", "between");
5954
+ case "IN":
5955
+ return tr("fluidgrids.rules.operator.IN", "in");
5956
+ case "NOT_IN":
5957
+ return tr("fluidgrids.rules.operator.NOT_IN", "not in");
5958
+ case "CONTAINS":
5959
+ return tr("fluidgrids.rules.operator.CONTAINS", "contains");
5960
+ case "NOT_CONTAINS":
5961
+ return tr("fluidgrids.rules.operator.NOT_CONTAINS", "does not contain");
5962
+ case "STARTS_WITH":
5963
+ return tr("fluidgrids.rules.operator.STARTS_WITH", "starts with");
5964
+ case "ENDS_WITH":
5965
+ return tr("fluidgrids.rules.operator.ENDS_WITH", "ends with");
5966
+ case "EXISTS":
5967
+ return tr("fluidgrids.rules.operator.EXISTS", "exists");
5968
+ case "NOT_EXISTS":
5969
+ return tr("fluidgrids.rules.operator.NOT_EXISTS", "is missing");
5970
+ case "IS_TRUE":
5971
+ return tr("fluidgrids.rules.operator.IS_TRUE", "is true");
5972
+ case "IS_FALSE":
5973
+ return tr("fluidgrids.rules.operator.IS_FALSE", "is false");
5974
+ default:
5975
+ return operator;
5976
+ }
5977
+ }
5978
+ function describeCondition(tr, condition) {
5979
+ const field = condition.field || "\u2026";
5980
+ const operator = operatorLabel(tr, condition.operator);
5981
+ if (UNARY_OPERATORS.has(condition.operator)) return `${field} ${operator}`;
5982
+ const value = isPlainObject(condition.value) ? typeof condition.value["$ref"] === "string" ? condition.value["$ref"] : JSON.stringify(condition.value) : formatValueText(condition.value) || "\u2026";
5983
+ return `${field} ${operator} ${value}`;
5984
+ }
5985
+ function summarizeNode(tr, type, config2) {
5986
+ switch (type) {
5987
+ case "CONDITION":
5988
+ return describeCondition(tr, configOf("CONDITION", config2).condition);
5989
+ case "COMPLEX": {
5990
+ const complex = configOf("COMPLEX", config2);
5991
+ const count = complex.conditions.length;
5992
+ if (complex.combinator === "ALL") {
5993
+ return count === 1 ? tr("fluidgrids.rules.summaryAllOne", "All of 1 condition") : tr("fluidgrids.rules.summaryAll", "All of {{count}} conditions", {
5994
+ count
5995
+ });
5996
+ }
5997
+ return count === 1 ? tr("fluidgrids.rules.summaryAnyOne", "Any of 1 condition") : tr("fluidgrids.rules.summaryAny", "Any of {{count}} conditions", {
5998
+ count
5999
+ });
6000
+ }
6001
+ case "SCORING": {
6002
+ const scoring = configOf("SCORING", config2);
6003
+ return scoring.rules.length === 1 ? tr(
6004
+ "fluidgrids.rules.summaryScoringOne",
6005
+ "1 scoring rule \u2192 {{variable}}",
6006
+ { variable: scoring.variable }
6007
+ ) : tr(
6008
+ "fluidgrids.rules.summaryScoring",
6009
+ "{{count}} scoring rules \u2192 {{variable}}",
6010
+ { count: scoring.rules.length, variable: scoring.variable }
6011
+ );
6012
+ }
6013
+ case "LOOKUP": {
6014
+ const lookup = configOf("LOOKUP", config2);
6015
+ const key = lookup.key || "\u2026";
6016
+ return lookup.source.kind === "DATA_TABLE" ? tr(
6017
+ "fluidgrids.rules.summaryLookupTable",
6018
+ "{{key}} \u2192 {{variable}} (data table)",
6019
+ { key, variable: lookup.variable }
6020
+ ) : lookup.source.entries.length === 1 ? tr(
6021
+ "fluidgrids.rules.summaryLookupInlineOne",
6022
+ "{{key}} \u2192 {{variable}} (1 entry)",
6023
+ { key, variable: lookup.variable }
6024
+ ) : tr(
6025
+ "fluidgrids.rules.summaryLookupInline",
6026
+ "{{key}} \u2192 {{variable}} ({{count}} entries)",
6027
+ {
6028
+ key,
6029
+ variable: lookup.variable,
6030
+ count: lookup.source.entries.length
6031
+ }
6032
+ );
6033
+ }
6034
+ case "DECISION":
6035
+ return configOf("DECISION", config2).outcome || "\u2026";
6036
+ }
6037
+ }
6038
+ function RuleBuilderHeader({
6039
+ version: version2,
6040
+ name,
6041
+ dirty,
6042
+ saving,
6043
+ canEdit,
6044
+ issues,
6045
+ onBack,
6046
+ onNameChange,
6047
+ onSave
6048
+ }) {
6049
+ const tr = useTr();
6050
+ const errorCount = issues.filter(
6051
+ (issue) => issue.severity === "ERROR"
6052
+ ).length;
6053
+ const warningCount = issues.length - errorCount;
6054
+ return /* @__PURE__ */ jsxs("header", { className: "flex flex-wrap items-center gap-3 border-b border-border-secondary px-4 py-3", children: [
6055
+ /* @__PURE__ */ jsx(
6056
+ Button$1,
6057
+ {
6058
+ variant: "ghost",
6059
+ size: "sm",
6060
+ onClick: onBack,
6061
+ "aria-label": tr("fluidgrids.rules.backToRuleSets", "Back to rule sets"),
6062
+ children: /* @__PURE__ */ jsx(ArrowLeft, { className: "size-4", "aria-hidden": "true" })
6063
+ }
6064
+ ),
6065
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
6066
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs text-text-secondary", children: [
6067
+ /* @__PURE__ */ jsx(Scale, { className: "size-3.5", "aria-hidden": "true" }),
6068
+ tr("fluidgrids.rules.ruleSet", "Rule set"),
6069
+ " \xB7 v",
6070
+ version2,
6071
+ dirty && /* @__PURE__ */ jsxs("span", { className: "text-status-warning", children: [
6072
+ "\xB7 ",
6073
+ tr("fluidgrids.rules.unsaved", "Unsaved changes")
6074
+ ] })
6075
+ ] }),
6076
+ /* @__PURE__ */ jsx(
6077
+ Input,
6078
+ {
6079
+ value: name,
6080
+ maxLength: 200,
6081
+ disabled: !canEdit,
6082
+ "aria-label": tr("fluidgrids.rules.name", "Name"),
6083
+ className: "h-8 max-w-md border-transparent px-1 text-lg font-semibold text-text-primary hover:border-input",
6084
+ onChange: (event) => onNameChange(event.target.value)
6085
+ }
6086
+ )
6087
+ ] }),
6088
+ (errorCount > 0 || warningCount > 0) && /* @__PURE__ */ jsxs(
6089
+ Badge,
6090
+ {
6091
+ variant: "outline",
6092
+ className: errorCount > 0 ? "text-status-error" : "text-status-warning",
6093
+ title: issues.map((issue) => issue.message).join("\n"),
6094
+ children: [
6095
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "mr-1 size-3.5", "aria-hidden": "true" }),
6096
+ errorCount > 0 ? tr("fluidgrids.rules.errorCount", "{{count}} errors", {
6097
+ count: errorCount
6098
+ }) : tr("fluidgrids.rules.warningCount", "{{count}} warnings", {
6099
+ count: warningCount
6100
+ })
6101
+ ]
6102
+ }
6103
+ ),
6104
+ /* @__PURE__ */ jsx(
6105
+ SdkPermissionButton,
6106
+ {
6107
+ permission: FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE,
6108
+ deniedTooltip: tr(
6109
+ "fluidgrids.rules.saveDenied",
6110
+ "You don't have permission to edit rule sets"
6111
+ ),
6112
+ children: /* @__PURE__ */ jsxs(Button$1, { onClick: onSave, disabled: saving || !dirty, children: [
6113
+ /* @__PURE__ */ jsx(Save, { className: "mr-2 size-4", "aria-hidden": "true" }),
6114
+ saving ? tr("fluidgrids.rules.saving", "Saving\u2026") : tr("fluidgrids.rules.save", "Save")
6115
+ ] })
6116
+ }
6117
+ )
6118
+ ] });
6119
+ }
6120
+ function RulePalette({
6121
+ canEdit,
6122
+ onAdd
6123
+ }) {
6124
+ const tr = useTr();
6125
+ return /* @__PURE__ */ jsxs(
6126
+ "aside",
6127
+ {
6128
+ className: "flex shrink-0 gap-2 overflow-x-auto border-b border-border-secondary p-3 lg:w-52 lg:flex-col lg:border-b-0 lg:border-r",
6129
+ "aria-label": tr("fluidgrids.rules.palette", "Blocks"),
6130
+ children: [
6131
+ /* @__PURE__ */ jsx("div", { className: "hidden text-xs font-medium uppercase tracking-wide text-text-secondary lg:block", children: tr("fluidgrids.rules.palette", "Blocks") }),
6132
+ RULE_NODE_TYPES.map((type) => {
6133
+ const style = RULE_BLOCK_STYLES[type];
6134
+ const Icon6 = style.icon;
6135
+ return /* @__PURE__ */ jsxs(
6136
+ "button",
6137
+ {
6138
+ type: "button",
6139
+ draggable: canEdit,
6140
+ disabled: !canEdit,
6141
+ onDragStart: (event) => {
6142
+ event.dataTransfer.setData(RULE_BLOCK_DRAG_MIME, type);
6143
+ event.dataTransfer.effectAllowed = "move";
6144
+ },
6145
+ onClick: () => onAdd(type),
6146
+ className: "flex shrink-0 items-center gap-2 rounded-md border border-border-secondary px-2 py-2 text-left text-sm text-text-primary hover:bg-muted/40 disabled:opacity-50",
6147
+ "data-testid": `rule-palette-${type.toLowerCase()}`,
6148
+ children: [
6149
+ /* @__PURE__ */ jsx(
6150
+ "span",
6151
+ {
6152
+ className: `flex size-6 items-center justify-center rounded ${style.chip}`,
6153
+ children: /* @__PURE__ */ jsx(Icon6, { className: "size-3.5", "aria-hidden": "true" })
6154
+ }
6155
+ ),
6156
+ ruleBlockTypeLabel(tr, type)
6157
+ ]
6158
+ },
6159
+ type
6160
+ );
6161
+ }),
6162
+ /* @__PURE__ */ jsx("p", { className: "hidden text-xs text-text-secondary lg:block", children: tr(
6163
+ "fluidgrids.rules.paletteHint",
6164
+ "Click or drag a block onto the canvas, then connect its outputs. Condition blocks have a TRUE and a FALSE output."
6165
+ ) })
6166
+ ]
6167
+ }
6168
+ );
6169
+ }
6170
+ function RuleIssueList({
6171
+ issues,
6172
+ onFocusNode
6173
+ }) {
6174
+ const tr = useTr();
6175
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 text-sm text-text-secondary", children: [
6176
+ /* @__PURE__ */ jsx("p", { children: tr("fluidgrids.rules.selectBlock", "Select a block to edit it.") }),
6177
+ issues.length > 0 && /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-1", children: issues.map((issue) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
6178
+ "button",
6179
+ {
6180
+ type: "button",
6181
+ className: `text-left text-xs ${issue.severity === "ERROR" ? "text-status-error" : "text-status-warning"}`,
6182
+ onClick: () => issue.nodeId && onFocusNode(issue.nodeId),
6183
+ children: issue.message
6184
+ }
6185
+ ) }, ruleIssueKey(issue))) })
6186
+ ] });
6187
+ }
6188
+ var HANDLE_OFFSETS = {
6189
+ TRUE: "30%",
6190
+ FALSE: "70%",
6191
+ NEXT: "50%"
6192
+ };
6193
+ function RuleNodeCardComponent({ data, selected }) {
6194
+ const tr = useTr();
6195
+ const style = RULE_BLOCK_STYLES[data.ruleType];
6196
+ const Icon6 = style.icon;
6197
+ const typeLabel = ruleBlockTypeLabel(tr, data.ruleType);
6198
+ const highlight = data.onPath ? "ring-2 ring-status-success" : selected ? "ring-2 ring-primary" : "";
6199
+ return /* @__PURE__ */ jsxs(
6200
+ "div",
6201
+ {
6202
+ className: `w-60 rounded-lg border-l-4 border border-border-secondary bg-background-primary shadow-sm ${style.accent} ${highlight}`,
6203
+ "data-testid": `rule-node-${data.ruleType.toLowerCase()}`,
6204
+ children: [
6205
+ /* @__PURE__ */ jsx(
6206
+ Handle,
6207
+ {
6208
+ type: "target",
6209
+ position: Position.Top,
6210
+ className: "!size-3 !border-2 !border-background-primary !bg-text-secondary"
6211
+ }
6212
+ ),
6213
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 p-3", children: [
6214
+ /* @__PURE__ */ jsx(
6215
+ "span",
6216
+ {
6217
+ className: `mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-md ${style.chip}`,
6218
+ children: /* @__PURE__ */ jsx(Icon6, { className: "size-4", "aria-hidden": "true" })
6219
+ }
6220
+ ),
6221
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
6222
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-text-secondary", children: [
6223
+ typeLabel,
6224
+ data.isStart && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-0.5 rounded bg-primary/10 px-1 text-primary normal-case", children: [
6225
+ /* @__PURE__ */ jsx(Play, { className: "size-3", "aria-hidden": "true" }),
6226
+ tr("fluidgrids.rules.start", "Start")
6227
+ ] })
6228
+ ] }),
6229
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-semibold text-text-primary", children: data.label || typeLabel }),
6230
+ /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-text-secondary", children: summarizeNode(tr, data.ruleType, data.config) }),
6231
+ data.stepOutcome && /* @__PURE__ */ jsx("div", { className: "mt-1 text-[11px] font-medium text-status-success", children: tr("fluidgrids.rules.stepResult", "Result: {{outcome}}", {
6232
+ outcome: data.stepOutcome
6233
+ }) })
6234
+ ] })
6235
+ ] }),
6236
+ branchesOf(data.ruleType).map((branch) => /* @__PURE__ */ jsx(
6237
+ Handle,
6238
+ {
6239
+ id: branch,
6240
+ type: "source",
6241
+ position: Position.Bottom,
6242
+ style: { left: HANDLE_OFFSETS[branch] },
6243
+ className: `!size-3 !border-2 !border-background-primary ${branch === "FALSE" ? "!bg-status-error" : branch === "TRUE" ? "!bg-status-success" : "!bg-text-secondary"}`,
6244
+ title: branch
6245
+ },
6246
+ branch
6247
+ )),
6248
+ branchesOf(data.ruleType).length > 1 && /* @__PURE__ */ jsxs("div", { className: "flex justify-between px-[22%] pb-1 text-[10px] font-semibold text-text-secondary", children: [
6249
+ /* @__PURE__ */ jsx("span", { children: tr("fluidgrids.rules.branchTrue", "TRUE") }),
6250
+ /* @__PURE__ */ jsx("span", { children: tr("fluidgrids.rules.branchFalse", "FALSE") })
6251
+ ] })
6252
+ ]
6253
+ }
6254
+ );
6255
+ }
6256
+ var RuleNodeCard = memo(RuleNodeCardComponent);
6257
+ var sequence = 0;
6258
+ function newRowKey() {
6259
+ sequence += 1;
6260
+ return `row-${sequence}`;
6261
+ }
6262
+ function useRowKeys(count) {
6263
+ const [keys, setKeys] = useState(
6264
+ () => Array.from({ length: count }, newRowKey)
6265
+ );
6266
+ let aligned = keys;
6267
+ if (keys.length !== count) {
6268
+ aligned = keys.length < count ? [...keys, ...Array.from({ length: count - keys.length }, newRowKey)] : keys.slice(0, count);
6269
+ setKeys(aligned);
6270
+ }
6271
+ const append = useCallback(
6272
+ () => setKeys((current) => [...current, newRowKey()]),
6273
+ []
6274
+ );
6275
+ const remove = useCallback(
6276
+ (position) => setKeys((current) => current.filter((_, i) => i !== position)),
6277
+ []
6278
+ );
6279
+ return { keys: aligned, append, remove };
6280
+ }
6281
+ var SELECT_CLASS = "flex h-9 w-full rounded-md border border-input bg-background px-2 text-sm text-text-primary";
6282
+ function ValueInput({
6283
+ value,
6284
+ onChange,
6285
+ ariaLabel,
6286
+ placeholder
6287
+ }) {
6288
+ const [text, setText] = useState(() => formatValueText(value));
6289
+ return /* @__PURE__ */ jsx(
6290
+ Input,
6291
+ {
6292
+ value: text,
6293
+ "aria-label": ariaLabel,
6294
+ placeholder,
6295
+ onChange: (event) => {
6296
+ setText(event.target.value);
6297
+ onChange(parseValueText(event.target.value));
6298
+ }
6299
+ }
6300
+ );
6301
+ }
6302
+ function ConditionRow({
6303
+ condition,
6304
+ onChange,
6305
+ onRemove
6306
+ }) {
6307
+ const tr = useTr();
6308
+ const unary = UNARY_OPERATORS.has(condition.operator);
6309
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded-md border border-border-secondary p-2", children: [
6310
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6311
+ /* @__PURE__ */ jsx(
6312
+ Input,
6313
+ {
6314
+ value: condition.field,
6315
+ "aria-label": tr("fluidgrids.rules.field", "Field"),
6316
+ placeholder: tr(
6317
+ "fluidgrids.rules.fieldPlaceholder",
6318
+ "applicant.creditScore"
6319
+ ),
6320
+ onChange: (event) => onChange({ ...condition, field: event.target.value })
6321
+ }
6322
+ ),
6323
+ onRemove && /* @__PURE__ */ jsx(
6324
+ Button$1,
6325
+ {
6326
+ variant: "ghost",
6327
+ size: "sm",
6328
+ onClick: onRemove,
6329
+ "aria-label": tr(
6330
+ "fluidgrids.rules.removeCondition",
6331
+ "Remove condition"
6332
+ ),
6333
+ children: /* @__PURE__ */ jsx(Trash2, { className: "size-4", "aria-hidden": "true" })
6334
+ }
6335
+ )
6336
+ ] }),
6337
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[8.5rem_minmax(0,1fr)] gap-2", children: [
6338
+ /* @__PURE__ */ jsx(
6339
+ "select",
6340
+ {
6341
+ className: SELECT_CLASS,
6342
+ value: condition.operator,
6343
+ "aria-label": tr("fluidgrids.rules.operator", "Operator"),
6344
+ onChange: (event) => {
6345
+ const operator = event.target.value;
6346
+ onChange(
6347
+ UNARY_OPERATORS.has(operator) ? { field: condition.field, operator } : { ...condition, operator, value: condition.value ?? "" }
6348
+ );
6349
+ },
6350
+ children: RULE_OPERATORS.map((operator) => /* @__PURE__ */ jsx("option", { value: operator, children: operatorLabel(tr, operator) }, operator))
6351
+ }
6352
+ ),
6353
+ !unary && /* @__PURE__ */ jsx(
6354
+ ValueInput,
6355
+ {
6356
+ value: condition.value,
6357
+ ariaLabel: tr("fluidgrids.rules.value", "Value"),
6358
+ placeholder: condition.operator === "BETWEEN" ? "[650, 749]" : condition.operator === "IN" || condition.operator === "NOT_IN" ? '["A", "B"]' : "750",
6359
+ onChange: (value) => onChange({ ...condition, value })
6360
+ }
6361
+ )
6362
+ ] })
6363
+ ] });
6364
+ }
6365
+ function isPlainObject2(value) {
6366
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6367
+ }
6368
+ function JsonField({
6369
+ label,
6370
+ value,
6371
+ onChange,
6372
+ rows = 4,
6373
+ validate
6374
+ }) {
6375
+ const [text, setText] = useState(() => JSON.stringify(value ?? {}, null, 2));
6376
+ const [error, setError] = useState(null);
6377
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6378
+ /* @__PURE__ */ jsx(Label, { children: label }),
6379
+ /* @__PURE__ */ jsx(
6380
+ Textarea,
6381
+ {
6382
+ rows,
6383
+ className: "font-mono text-xs",
6384
+ value: text,
6385
+ "aria-label": label,
6386
+ onChange: (event) => {
6387
+ setText(event.target.value);
6388
+ const parsed = parseJsonText(event.target.value);
6389
+ if (parsed.ok) {
6390
+ const invalid = validate?.(parsed.value) ?? null;
6391
+ setError(invalid);
6392
+ if (invalid === null) onChange(parsed.value);
6393
+ } else {
6394
+ setError(parsed.error);
6395
+ }
6396
+ }
6397
+ }
6398
+ ),
6399
+ error && /* @__PURE__ */ jsx("p", { className: "text-xs text-status-error", children: error })
6400
+ ] });
6401
+ }
6402
+ function RuleInspector({
6403
+ node,
6404
+ isStart,
6405
+ readOnly,
6406
+ dataTables,
6407
+ onChange,
6408
+ onDelete,
6409
+ onMakeStart
6410
+ }) {
6411
+ const tr = useTr();
6412
+ const { ruleType, config: config2 } = node.data;
6413
+ const setConfig = (next) => onChange({ config: next });
6414
+ return /* @__PURE__ */ jsxs(
6415
+ "fieldset",
6416
+ {
6417
+ className: "flex flex-col gap-3",
6418
+ disabled: readOnly,
6419
+ "aria-label": tr("fluidgrids.rules.blockSettings", "Block settings"),
6420
+ children: [
6421
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
6422
+ /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium uppercase tracking-wide text-text-secondary", children: [
6423
+ ruleBlockTypeLabel(tr, ruleType),
6424
+ " \xB7 ",
6425
+ node.id
6426
+ ] }),
6427
+ /* @__PURE__ */ jsx(
6428
+ Button$1,
6429
+ {
6430
+ variant: "ghost",
6431
+ size: "sm",
6432
+ onClick: onDelete,
6433
+ "aria-label": tr("fluidgrids.rules.deleteBlock", "Delete block"),
6434
+ children: /* @__PURE__ */ jsx(Trash2, { className: "size-4", "aria-hidden": "true" })
6435
+ }
6436
+ )
6437
+ ] }),
6438
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6439
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-block-label", children: tr("fluidgrids.rules.label", "Label") }),
6440
+ /* @__PURE__ */ jsx(
6441
+ Input,
6442
+ {
6443
+ id: "rule-block-label",
6444
+ value: node.data.label,
6445
+ maxLength: 120,
6446
+ onChange: (event) => onChange({ label: event.target.value })
6447
+ }
6448
+ )
6449
+ ] }),
6450
+ !isStart && /* @__PURE__ */ jsxs(Button$1, { variant: "outline", size: "sm", onClick: onMakeStart, children: [
6451
+ /* @__PURE__ */ jsx(Play, { className: "mr-2 size-4", "aria-hidden": "true" }),
6452
+ tr("fluidgrids.rules.makeStart", "Start evaluation here")
6453
+ ] }),
6454
+ ruleType === "CONDITION" && /* @__PURE__ */ jsx(
6455
+ ConditionRow,
6456
+ {
6457
+ condition: configOf("CONDITION", config2).condition,
6458
+ onChange: (condition) => setConfig({ condition })
6459
+ }
6460
+ ),
6461
+ ruleType === "COMPLEX" && /* @__PURE__ */ jsx(
6462
+ ComplexEditor,
6463
+ {
6464
+ config: configOf("COMPLEX", config2),
6465
+ onChange: setConfig
6466
+ }
6467
+ ),
6468
+ ruleType === "SCORING" && /* @__PURE__ */ jsx(
6469
+ ScoringEditor,
6470
+ {
6471
+ config: configOf("SCORING", config2),
6472
+ onChange: setConfig
6473
+ }
6474
+ ),
6475
+ ruleType === "LOOKUP" && /* @__PURE__ */ jsx(
6476
+ LookupEditor,
6477
+ {
6478
+ config: configOf("LOOKUP", config2),
6479
+ dataTables,
6480
+ onChange: setConfig
6481
+ }
6482
+ ),
6483
+ ruleType === "DECISION" && /* @__PURE__ */ jsx(
6484
+ DecisionEditor,
6485
+ {
6486
+ config: configOf("DECISION", config2),
6487
+ onChange: setConfig
6488
+ },
6489
+ node.id
6490
+ )
6491
+ ]
6492
+ }
6493
+ );
6494
+ }
6495
+ function ComplexEditor({
6496
+ config: config2,
6497
+ onChange
6498
+ }) {
6499
+ const tr = useTr();
6500
+ const rows = useRowKeys(config2.conditions.length);
6501
+ const update = (index, condition) => onChange({
6502
+ ...config2,
6503
+ conditions: config2.conditions.map(
6504
+ (item, i) => i === index ? condition : item
6505
+ )
6506
+ });
6507
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
6508
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-combinator", children: tr("fluidgrids.rules.combinator", "Match") }),
6509
+ /* @__PURE__ */ jsxs(
6510
+ "select",
6511
+ {
6512
+ id: "rule-combinator",
6513
+ className: SELECT_CLASS,
6514
+ value: config2.combinator,
6515
+ onChange: (event) => onChange({
6516
+ ...config2,
6517
+ combinator: event.target.value === "ANY" ? "ANY" : "ALL"
6518
+ }),
6519
+ children: [
6520
+ /* @__PURE__ */ jsx("option", { value: "ALL", children: tr("fluidgrids.rules.combinatorAll", "All conditions (AND)") }),
6521
+ /* @__PURE__ */ jsx("option", { value: "ANY", children: tr("fluidgrids.rules.combinatorAny", "Any condition (OR)") })
6522
+ ]
6523
+ }
6524
+ ),
6525
+ config2.conditions.map((condition, position) => /* @__PURE__ */ jsx(
6526
+ ConditionRow,
6527
+ {
6528
+ condition,
6529
+ onChange: (next) => update(position, next),
6530
+ onRemove: () => {
6531
+ rows.remove(position);
6532
+ onChange({
6533
+ ...config2,
6534
+ conditions: config2.conditions.filter((_, i) => i !== position)
6535
+ });
6536
+ }
6537
+ },
6538
+ rows.keys[position]
6539
+ )),
6540
+ /* @__PURE__ */ jsxs(
6541
+ Button$1,
6542
+ {
6543
+ variant: "outline",
6544
+ size: "sm",
6545
+ onClick: () => {
6546
+ rows.append();
6547
+ onChange({
6548
+ ...config2,
6549
+ conditions: [
6550
+ ...config2.conditions,
6551
+ { field: "", operator: "EQ", value: "" }
6552
+ ]
6553
+ });
6554
+ },
6555
+ children: [
6556
+ /* @__PURE__ */ jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }),
6557
+ tr("fluidgrids.rules.addCondition", "Add condition")
6558
+ ]
6559
+ }
6560
+ )
6561
+ ] });
6562
+ }
6563
+ function ScoringEditor({
6564
+ config: config2,
6565
+ onChange
6566
+ }) {
6567
+ const tr = useTr();
6568
+ const rows = useRowKeys(config2.rules.length);
6569
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
6570
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
6571
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6572
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-score-variable", children: tr("fluidgrids.rules.variable", "Variable") }),
6573
+ /* @__PURE__ */ jsx(
6574
+ Input,
6575
+ {
6576
+ id: "rule-score-variable",
6577
+ value: config2.variable,
6578
+ onChange: (event) => onChange({ ...config2, variable: event.target.value })
6579
+ }
6580
+ )
6581
+ ] }),
6582
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6583
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-score-base", children: tr("fluidgrids.rules.basePoints", "Base points") }),
6584
+ /* @__PURE__ */ jsx(
6585
+ Input,
6586
+ {
6587
+ id: "rule-score-base",
6588
+ type: "number",
6589
+ value: String(config2.base),
6590
+ onChange: (event) => onChange({ ...config2, base: Number(event.target.value) || 0 })
6591
+ }
6592
+ )
6593
+ ] })
6594
+ ] }),
6595
+ config2.rules.map((rule, position) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6596
+ /* @__PURE__ */ jsx(
6597
+ ConditionRow,
6598
+ {
6599
+ condition: rule.condition,
6600
+ onChange: (condition) => onChange({
6601
+ ...config2,
6602
+ rules: config2.rules.map(
6603
+ (item, i) => i === position ? { ...item, condition } : item
6604
+ )
6605
+ }),
6606
+ onRemove: () => {
6607
+ rows.remove(position);
6608
+ onChange({
6609
+ ...config2,
6610
+ rules: config2.rules.filter((_, i) => i !== position)
6611
+ });
6612
+ }
6613
+ }
6614
+ ),
6615
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pl-2", children: [
6616
+ /* @__PURE__ */ jsx(
6617
+ Label,
6618
+ {
6619
+ className: "text-xs",
6620
+ htmlFor: `rule-points-${rows.keys[position]}`,
6621
+ children: tr("fluidgrids.rules.points", "Points")
6622
+ }
6623
+ ),
6624
+ /* @__PURE__ */ jsx(
6625
+ Input,
6626
+ {
6627
+ id: `rule-points-${rows.keys[position]}`,
6628
+ type: "number",
6629
+ className: "h-8 w-24",
6630
+ value: String(rule.points),
6631
+ onChange: (event) => onChange({
6632
+ ...config2,
6633
+ rules: config2.rules.map(
6634
+ (item, i) => i === position ? { ...item, points: Number(event.target.value) || 0 } : item
6635
+ )
6636
+ })
6637
+ }
6638
+ )
6639
+ ] })
6640
+ ] }, rows.keys[position])),
6641
+ /* @__PURE__ */ jsxs(
6642
+ Button$1,
6643
+ {
6644
+ variant: "outline",
6645
+ size: "sm",
6646
+ onClick: () => {
6647
+ rows.append();
6648
+ onChange({
6649
+ ...config2,
6650
+ rules: [
6651
+ ...config2.rules,
6652
+ {
6653
+ condition: { field: "", operator: "GTE", value: 0 },
6654
+ points: 10
6655
+ }
6656
+ ]
6657
+ });
6658
+ },
6659
+ children: [
6660
+ /* @__PURE__ */ jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }),
6661
+ tr("fluidgrids.rules.addScoringRule", "Add scoring rule")
6662
+ ]
6663
+ }
6664
+ )
6665
+ ] });
6666
+ }
6667
+ function LookupEditor({
6668
+ config: config2,
6669
+ dataTables,
6670
+ onChange
6671
+ }) {
6672
+ const tr = useTr();
6673
+ const source = config2.source;
6674
+ const rows = useRowKeys(source.kind === "INLINE" ? source.entries.length : 0);
6675
+ const selectedTable = source.kind === "DATA_TABLE" ? dataTables.find((table) => table.id === source.tableId) : void 0;
6676
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
6677
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6678
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-lookup-key", children: tr("fluidgrids.rules.lookupKey", "Key field") }),
6679
+ /* @__PURE__ */ jsx(
6680
+ Input,
6681
+ {
6682
+ id: "rule-lookup-key",
6683
+ value: config2.key,
6684
+ placeholder: "applicant.employmentType",
6685
+ onChange: (event) => onChange({ ...config2, key: event.target.value })
6686
+ }
6687
+ )
6688
+ ] }),
6689
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
6690
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6691
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-lookup-variable", children: tr("fluidgrids.rules.variable", "Variable") }),
6692
+ /* @__PURE__ */ jsx(
6693
+ Input,
6694
+ {
6695
+ id: "rule-lookup-variable",
6696
+ value: config2.variable,
6697
+ onChange: (event) => onChange({ ...config2, variable: event.target.value })
6698
+ }
6699
+ )
6700
+ ] }),
6701
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6702
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-lookup-default", children: tr("fluidgrids.rules.defaultValue", "Default") }),
6703
+ /* @__PURE__ */ jsx(
6704
+ ValueInput,
6705
+ {
6706
+ value: config2.defaultValue,
6707
+ ariaLabel: tr("fluidgrids.rules.defaultValue", "Default"),
6708
+ onChange: (defaultValue) => onChange({ ...config2, defaultValue })
6709
+ }
6710
+ )
6711
+ ] })
6712
+ ] }),
6713
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-lookup-source", children: tr("fluidgrids.rules.lookupSource", "Source") }),
6714
+ /* @__PURE__ */ jsxs(
6715
+ "select",
6716
+ {
6717
+ id: "rule-lookup-source",
6718
+ className: SELECT_CLASS,
6719
+ value: source.kind,
6720
+ onChange: (event) => onChange({
6721
+ ...config2,
6722
+ source: event.target.value === "DATA_TABLE" ? {
6723
+ kind: "DATA_TABLE",
6724
+ tableId: dataTables[0]?.id ?? "",
6725
+ keyColumn: dataTables[0]?.columns[0]?.key ?? "",
6726
+ valueColumn: dataTables[0]?.columns[1]?.key ?? ""
6727
+ } : { kind: "INLINE", entries: [] }
6728
+ }),
6729
+ children: [
6730
+ /* @__PURE__ */ jsx("option", { value: "INLINE", children: tr("fluidgrids.rules.sourceInline", "Entries in this block") }),
6731
+ /* @__PURE__ */ jsx("option", { value: "DATA_TABLE", children: tr("fluidgrids.rules.sourceDataTable", "A data table") })
6732
+ ]
6733
+ }
6734
+ ),
6735
+ source.kind === "INLINE" ? /* @__PURE__ */ jsxs(Fragment, { children: [
6736
+ source.entries.map((entry, position) => /* @__PURE__ */ jsxs(
6737
+ "div",
6738
+ {
6739
+ className: "grid grid-cols-[1fr_1fr_auto] gap-2",
6740
+ children: [
6741
+ /* @__PURE__ */ jsx(
6742
+ Input,
6743
+ {
6744
+ value: entry.key,
6745
+ "aria-label": tr("fluidgrids.rules.entryKey", "Key"),
6746
+ onChange: (event) => onChange({
6747
+ ...config2,
6748
+ source: {
6749
+ kind: "INLINE",
6750
+ entries: source.entries.map(
6751
+ (item, i) => i === position ? { ...item, key: event.target.value } : item
6752
+ )
6753
+ }
6754
+ })
6755
+ }
6756
+ ),
6757
+ /* @__PURE__ */ jsx(
6758
+ ValueInput,
6759
+ {
6760
+ value: entry.value,
6761
+ ariaLabel: tr("fluidgrids.rules.entryValue", "Value"),
6762
+ onChange: (value) => onChange({
6763
+ ...config2,
6764
+ source: {
6765
+ kind: "INLINE",
6766
+ entries: source.entries.map(
6767
+ (item, i) => i === position ? { ...item, value } : item
6768
+ )
6769
+ }
6770
+ })
6771
+ }
6772
+ ),
6773
+ /* @__PURE__ */ jsx(
6774
+ Button$1,
6775
+ {
6776
+ variant: "ghost",
6777
+ size: "sm",
6778
+ "aria-label": tr("fluidgrids.rules.removeEntry", "Remove entry"),
6779
+ onClick: () => {
6780
+ rows.remove(position);
6781
+ onChange({
6782
+ ...config2,
6783
+ source: {
6784
+ kind: "INLINE",
6785
+ entries: source.entries.filter((_, i) => i !== position)
6786
+ }
6787
+ });
6788
+ },
6789
+ children: /* @__PURE__ */ jsx(Trash2, { className: "size-4", "aria-hidden": "true" })
6790
+ }
6791
+ )
6792
+ ]
6793
+ },
6794
+ rows.keys[position]
6795
+ )),
6796
+ /* @__PURE__ */ jsxs(
6797
+ Button$1,
6798
+ {
6799
+ variant: "outline",
6800
+ size: "sm",
6801
+ onClick: () => {
6802
+ rows.append();
6803
+ onChange({
6804
+ ...config2,
6805
+ source: {
6806
+ kind: "INLINE",
6807
+ entries: [...source.entries, { key: "", value: "" }]
6808
+ }
6809
+ });
6810
+ },
6811
+ children: [
6812
+ /* @__PURE__ */ jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }),
6813
+ tr("fluidgrids.rules.addEntry", "Add entry")
6814
+ ]
6815
+ }
6816
+ )
6817
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
6818
+ dataTables.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-xs text-text-secondary", children: tr(
6819
+ "fluidgrids.rules.noDataTables",
6820
+ "This workspace has no data tables yet. Create one under Data Tables."
6821
+ ) }),
6822
+ /* @__PURE__ */ jsx(
6823
+ "select",
6824
+ {
6825
+ className: SELECT_CLASS,
6826
+ "aria-label": tr("fluidgrids.rules.dataTable", "Data table"),
6827
+ value: source.tableId,
6828
+ onChange: (event) => {
6829
+ const table = dataTables.find(
6830
+ (item) => item.id === event.target.value
6831
+ );
6832
+ onChange({
6833
+ ...config2,
6834
+ source: {
6835
+ kind: "DATA_TABLE",
6836
+ tableId: event.target.value,
6837
+ keyColumn: table?.columns[0]?.key ?? "",
6838
+ valueColumn: table?.columns[1]?.key ?? ""
6839
+ }
6840
+ });
6841
+ },
6842
+ children: dataTables.map((table) => /* @__PURE__ */ jsx("option", { value: table.id, children: table.name }, table.id))
6843
+ }
6844
+ ),
6845
+ ["keyColumn", "valueColumn"].map((column) => /* @__PURE__ */ jsx(
6846
+ "select",
6847
+ {
6848
+ className: SELECT_CLASS,
6849
+ "aria-label": column === "keyColumn" ? tr("fluidgrids.rules.keyColumn", "Key column") : tr("fluidgrids.rules.valueColumn", "Value column"),
6850
+ value: source[column],
6851
+ onChange: (event) => onChange({
6852
+ ...config2,
6853
+ source: { ...source, [column]: event.target.value }
6854
+ }),
6855
+ children: (selectedTable?.columns ?? []).map((item) => /* @__PURE__ */ jsx("option", { value: item.key, children: column === "keyColumn" ? tr("fluidgrids.rules.keyColumnOption", "Key: {{label}}", {
6856
+ label: item.label
6857
+ }) : tr(
6858
+ "fluidgrids.rules.valueColumnOption",
6859
+ "Value: {{label}}",
6860
+ { label: item.label }
6861
+ ) }, item.key))
6862
+ },
6863
+ column
6864
+ ))
6865
+ ] })
6866
+ ] });
6867
+ }
6868
+ function DecisionEditor({
6869
+ config: config2,
6870
+ onChange
6871
+ }) {
6872
+ const tr = useTr();
6873
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
6874
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6875
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-outcome", children: tr("fluidgrids.rules.outcome", "Outcome") }),
6876
+ /* @__PURE__ */ jsx(
6877
+ Input,
6878
+ {
6879
+ id: "rule-outcome",
6880
+ value: config2.outcome,
6881
+ maxLength: 80,
6882
+ onChange: (event) => onChange({ ...config2, outcome: event.target.value })
6883
+ }
6884
+ )
6885
+ ] }),
6886
+ /* @__PURE__ */ jsx(
6887
+ JsonField,
6888
+ {
6889
+ label: tr("fluidgrids.rules.output", "Output (JSON object)"),
6890
+ value: config2.output,
6891
+ validate: (output) => isPlainObject2(output) ? null : tr(
6892
+ "fluidgrids.rules.outputMustBeObject",
6893
+ 'The output must be a JSON object, for example {"approved": true}.'
6894
+ ),
6895
+ onChange: (output) => {
6896
+ if (isPlainObject2(output)) onChange({ ...config2, output });
6897
+ }
6898
+ }
6899
+ )
6900
+ ] });
6901
+ }
6902
+
6903
+ // src/modules/rule-builder/lib/content-keys.ts
6904
+ function contentKeys(items, keyOf) {
6905
+ const seen = /* @__PURE__ */ new Map();
6906
+ return items.map((item) => {
6907
+ const base = keyOf(item);
6908
+ const count = seen.get(base) ?? 0;
6909
+ seen.set(base, count + 1);
6910
+ return count === 0 ? base : `${base}#${count}`;
6911
+ });
6912
+ }
6913
+ function formatJson(value) {
6914
+ return value === void 0 ? "null" : JSON.stringify(value);
6915
+ }
6916
+ function issueKey(issue) {
6917
+ return `${issue.code}:${issue.nodeId ?? ""}:${issue.message}`;
6918
+ }
6919
+ function conditionKey(condition) {
6920
+ return `${condition.field}:${condition.operator}:${formatJson(condition.expected)}`;
6921
+ }
6922
+ function TraceConditionList({
6923
+ conditions
6924
+ }) {
6925
+ const tr = useTr();
6926
+ const keys = contentKeys(conditions, conditionKey);
6927
+ return /* @__PURE__ */ jsx("ul", { className: "mt-1 flex flex-col gap-0.5", children: conditions.map((condition, position) => /* @__PURE__ */ jsxs(
6928
+ "li",
6929
+ {
6930
+ className: "flex items-center gap-1 text-[11px] text-text-secondary",
6931
+ children: [
6932
+ condition.passed ? /* @__PURE__ */ jsx(
6933
+ CheckCircle2,
6934
+ {
6935
+ className: "size-3 text-status-success",
6936
+ "aria-hidden": "true"
6937
+ }
6938
+ ) : /* @__PURE__ */ jsx(XCircle, { className: "size-3 text-status-error", "aria-hidden": "true" }),
6939
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
6940
+ condition.field,
6941
+ " ",
6942
+ operatorLabel(tr, condition.operator),
6943
+ " ",
6944
+ formatJson(condition.expected),
6945
+ " (",
6946
+ formatJson(condition.actual),
6947
+ ")"
6948
+ ] })
6949
+ ]
6950
+ },
6951
+ keys[position]
6952
+ )) });
6953
+ }
6954
+ function RuleTestPanel({
6955
+ inputText,
6956
+ onInputTextChange,
6957
+ onRun,
6958
+ running,
6959
+ result,
6960
+ onSelectStep
6961
+ }) {
6962
+ const tr = useTr();
6963
+ const [error, setError] = useState(null);
6964
+ const issueKeys = contentKeys(result?.issues ?? [], issueKey);
6965
+ const run = () => {
6966
+ const parsed = parseJsonText(inputText);
6967
+ if (!parsed.ok) {
6968
+ setError(parsed.error);
6969
+ return;
6970
+ }
6971
+ setError(null);
6972
+ onRun(parsed.value);
6973
+ };
6974
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", "data-testid": "rule-test-panel", children: [
6975
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
6976
+ /* @__PURE__ */ jsx(Label, { htmlFor: "rule-test-input", children: tr("fluidgrids.rules.sampleInput", "Sample input (JSON)") }),
6977
+ /* @__PURE__ */ jsx(
6978
+ Textarea,
6979
+ {
6980
+ id: "rule-test-input",
6981
+ rows: 10,
6982
+ className: "font-mono text-xs",
6983
+ value: inputText,
6984
+ onChange: (event) => onInputTextChange(event.target.value)
6985
+ }
6986
+ ),
6987
+ error && /* @__PURE__ */ jsx("p", { className: "text-xs text-status-error", children: error })
6988
+ ] }),
6989
+ /* @__PURE__ */ jsxs(Button$1, { onClick: run, disabled: running, children: [
6990
+ /* @__PURE__ */ jsx(FlaskConical, { className: "mr-2 size-4", "aria-hidden": "true" }),
6991
+ running ? tr("fluidgrids.rules.testing", "Testing\u2026") : tr("fluidgrids.rules.runTest", "Test rule")
6992
+ ] }),
6993
+ result && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", "data-testid": "rule-test-result", children: [
6994
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-border-secondary p-3", children: [
6995
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-text-secondary", children: tr("fluidgrids.rules.decision", "Decision") }),
6996
+ result.status === "DECIDED" ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6997
+ /* @__PURE__ */ jsx(
6998
+ CheckCircle2,
6999
+ {
7000
+ className: "size-5 text-status-success",
7001
+ "aria-hidden": "true"
7002
+ }
7003
+ ),
7004
+ /* @__PURE__ */ jsx(
7005
+ "span",
7006
+ {
7007
+ className: "text-lg font-semibold text-text-primary",
7008
+ "data-testid": "rule-test-decision",
7009
+ children: result.decision
7010
+ }
7011
+ )
7012
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-status-warning", children: [
7013
+ /* @__PURE__ */ jsx(CircleSlash, { className: "size-5", "aria-hidden": "true" }),
7014
+ /* @__PURE__ */ jsx("span", { className: "font-medium", "data-testid": "rule-test-decision", children: result.status === "INVALID" ? tr(
7015
+ "fluidgrids.rules.resultInvalid",
7016
+ "Not evaluated: fix the errors below"
7017
+ ) : tr(
7018
+ "fluidgrids.rules.resultNoDecision",
7019
+ "No decision reached"
7020
+ ) })
7021
+ ] }),
7022
+ /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs text-text-secondary", children: tr(
7023
+ "fluidgrids.rules.evaluatedIn",
7024
+ "{{steps}} steps in {{ms}} ms",
7025
+ { steps: result.trace.length, ms: result.durationMs }
7026
+ ) }),
7027
+ result.output !== null && result.output !== void 0 && /* @__PURE__ */ jsx("pre", { className: "mt-2 overflow-x-auto rounded bg-muted/40 p-2 text-xs text-text-primary", children: JSON.stringify(result.output, null, 2) })
7028
+ ] }),
7029
+ result.issues.length > 0 && /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-1", children: result.issues.map((issue, position) => /* @__PURE__ */ jsx(
7030
+ "li",
7031
+ {
7032
+ className: `text-xs ${issue.severity === "ERROR" ? "text-status-error" : "text-status-warning"}`,
7033
+ children: issue.message
7034
+ },
7035
+ issueKeys[position]
7036
+ )) }),
7037
+ result.trace.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
7038
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-text-secondary", children: tr("fluidgrids.rules.trace", "Evaluation trace") }),
7039
+ /* @__PURE__ */ jsx("ol", { className: "flex flex-col gap-2", children: result.trace.map((step) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
7040
+ "button",
7041
+ {
7042
+ type: "button",
7043
+ onClick: () => onSelectStep(step.nodeId),
7044
+ className: "w-full rounded-md border border-border-secondary p-2 text-left hover:bg-muted/40",
7045
+ children: [
7046
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
7047
+ /* @__PURE__ */ jsxs("span", { className: "truncate text-sm font-medium text-text-primary", children: [
7048
+ step.index + 1,
7049
+ ". ",
7050
+ step.label || step.nodeId
7051
+ ] }),
7052
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: step.outcome })
7053
+ ] }),
7054
+ /* @__PURE__ */ jsx("div", { className: "text-[11px] text-text-secondary", children: ruleBlockTypeLabel(tr, step.nodeType) }),
7055
+ /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs text-text-secondary", children: step.detail }),
7056
+ step.conditions.length > 1 && /* @__PURE__ */ jsx(TraceConditionList, { conditions: step.conditions })
7057
+ ]
7058
+ }
7059
+ ) }, `${step.index}-${step.nodeId}`)) })
7060
+ ] })
7061
+ ] })
7062
+ ] });
7063
+ }
7064
+ function RuleSidePanel({
7065
+ panel,
7066
+ onPanelChange,
7067
+ selectedNode,
7068
+ effectiveStart,
7069
+ canEdit,
7070
+ dataTables,
7071
+ onChangeSelected,
7072
+ onDeleteSelected,
7073
+ onMakeStart,
7074
+ issues,
7075
+ onFocusNode,
7076
+ inputText,
7077
+ onInputTextChange,
7078
+ onRunTest,
7079
+ testing,
7080
+ result
7081
+ }) {
7082
+ const tr = useTr();
7083
+ return /* @__PURE__ */ jsxs("aside", { className: "flex max-h-[50vh] shrink-0 flex-col border-t border-border-secondary lg:max-h-none lg:w-96 lg:border-l lg:border-t-0", children: [
7084
+ /* @__PURE__ */ jsx("div", { className: "flex border-b border-border-secondary", role: "tablist", children: ["block", "test"].map((tab) => /* @__PURE__ */ jsx(
7085
+ "button",
7086
+ {
7087
+ type: "button",
7088
+ role: "tab",
7089
+ "aria-selected": panel === tab,
7090
+ onClick: () => onPanelChange(tab),
7091
+ className: `flex-1 px-3 py-2 text-sm font-medium ${panel === tab ? "border-b-2 border-primary text-text-primary" : "text-text-secondary"}`,
7092
+ "data-testid": `rule-tab-${tab}`,
7093
+ children: tab === "block" ? tr("fluidgrids.rules.tabBlock", "Block") : tr("fluidgrids.rules.tabTest", "Test rule")
7094
+ },
7095
+ tab
7096
+ )) }),
7097
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children: [
7098
+ panel === "block" && (selectedNode ? /* @__PURE__ */ jsx(
7099
+ RuleInspector,
7100
+ {
7101
+ node: selectedNode,
7102
+ isStart: selectedNode.id === effectiveStart,
7103
+ readOnly: !canEdit,
7104
+ dataTables,
7105
+ onChange: onChangeSelected,
7106
+ onDelete: onDeleteSelected,
7107
+ onMakeStart: () => onMakeStart(selectedNode.id)
7108
+ },
7109
+ selectedNode.id
7110
+ ) : /* @__PURE__ */ jsx(RuleIssueList, { issues, onFocusNode })),
7111
+ panel === "test" && /* @__PURE__ */ jsx(
7112
+ RuleTestPanel,
7113
+ {
7114
+ inputText,
7115
+ onInputTextChange,
7116
+ onRun: onRunTest,
7117
+ running: testing,
7118
+ result,
7119
+ onSelectStep: onFocusNode
7120
+ }
7121
+ )
7122
+ ] })
7123
+ ] });
7124
+ }
7125
+ var NODE_TYPES = { rule: RuleNodeCard };
7126
+ var DEFAULT_EDGE_OPTIONS = {
7127
+ markerEnd: { type: MarkerType.ArrowClosed }
7128
+ };
7129
+ function useDecoratedGraph(nodes, edges, startNodeId, result) {
7130
+ const effectiveStart = useMemo(
7131
+ () => resolveStartNodeId(fromFlow(nodes, edges, startNodeId)),
7132
+ [nodes, edges, startNodeId]
7133
+ );
7134
+ const displayNodes = useMemo(() => {
7135
+ const steps = /* @__PURE__ */ new Map();
7136
+ for (const step of result?.trace ?? [])
7137
+ steps.set(step.nodeId, step.outcome);
7138
+ return nodes.map((node) => {
7139
+ const isStart = node.id === effectiveStart;
7140
+ const stepOutcome = steps.get(node.id);
7141
+ if (node.data.isStart === isStart && node.data.stepOutcome === stepOutcome)
7142
+ return node;
7143
+ return {
7144
+ ...node,
7145
+ data: {
7146
+ ...node.data,
7147
+ isStart,
7148
+ onPath: stepOutcome !== void 0,
7149
+ stepOutcome
7150
+ }
7151
+ };
7152
+ });
7153
+ }, [nodes, effectiveStart, result]);
7154
+ const displayEdges = useMemo(() => {
7155
+ const path = result?.path ?? [];
7156
+ const onPath = new Set(
7157
+ path.slice(1).map((target, index) => `${path[index]}->${target}`)
7158
+ );
7159
+ const taken = /* @__PURE__ */ new Map();
7160
+ for (const step of result?.trace ?? [])
7161
+ taken.set(step.nodeId, step.outcome);
7162
+ return edges.map((edge) => {
7163
+ const branch = edge.data?.branch ?? branchFromHandle(edge.sourceHandle);
7164
+ const active = onPath.has(`${edge.source}->${edge.target}`) && taken.get(edge.source) === branch;
7165
+ return active === Boolean(edge.animated) ? edge : { ...edge, animated: active };
7166
+ });
7167
+ }, [edges, result]);
7168
+ return { effectiveStart, displayNodes, displayEdges };
7169
+ }
7170
+ function RuleCanvas({
7171
+ nodes,
7172
+ edges,
7173
+ isEmpty,
7174
+ canEdit,
7175
+ colorMode,
7176
+ onNodesChange,
7177
+ onEdgesChange,
7178
+ onConnect,
7179
+ onDrop,
7180
+ onNodeClick,
7181
+ onNodesDelete,
7182
+ onPaneClick
7183
+ }) {
7184
+ const tr = useTr();
7185
+ return /* @__PURE__ */ jsxs(
7186
+ "div",
7187
+ {
7188
+ className: "relative min-h-[360px] flex-1",
7189
+ onDragOver: (event) => {
7190
+ event.preventDefault();
7191
+ event.dataTransfer.dropEffect = "move";
7192
+ },
7193
+ onDrop,
7194
+ "data-testid": "rule-canvas",
7195
+ children: [
7196
+ /* @__PURE__ */ jsxs(
7197
+ ReactFlow,
7198
+ {
7199
+ nodes,
7200
+ edges,
7201
+ nodeTypes: NODE_TYPES,
7202
+ defaultEdgeOptions: DEFAULT_EDGE_OPTIONS,
7203
+ colorMode,
7204
+ onNodesChange,
7205
+ onEdgesChange,
7206
+ onConnect,
7207
+ onNodeClick: (_event, node) => onNodeClick(node.id),
7208
+ onNodesDelete: (deleted) => onNodesDelete(deleted.map((node) => node.id)),
7209
+ onPaneClick,
7210
+ nodesDraggable: canEdit,
7211
+ nodesConnectable: canEdit,
7212
+ elementsSelectable: true,
7213
+ deleteKeyCode: canEdit ? ["Backspace", "Delete"] : null,
7214
+ fitView: true,
7215
+ fitViewOptions: { padding: 0.2 },
7216
+ proOptions: { hideAttribution: true },
7217
+ children: [
7218
+ /* @__PURE__ */ jsx(Background, { gap: 20 }),
7219
+ /* @__PURE__ */ jsx(Controls, { showInteractive: false }),
7220
+ /* @__PURE__ */ jsx(MiniMap, { pannable: true, zoomable: true, className: "!hidden md:!block" })
7221
+ ]
7222
+ }
7223
+ ),
7224
+ isEmpty && /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center p-6 text-center text-sm text-text-secondary", children: tr(
7225
+ "fluidgrids.rules.emptyCanvas",
7226
+ "Add a block from the palette to start building this rule set."
7227
+ ) })
7228
+ ]
7229
+ }
7230
+ );
7231
+ }
7232
+ function useRuleTestRunner() {
7233
+ const tr = useTr();
7234
+ const { testRuleSet } = useRuleSetMutations();
7235
+ const revision = useRef(0);
7236
+ const [testing, setTesting] = useState(false);
7237
+ const [result, setResult] = useState(null);
7238
+ const invalidate = useCallback(() => {
7239
+ revision.current += 1;
7240
+ setResult(null);
7241
+ }, []);
7242
+ const runTest = useCallback(
7243
+ async (definition, input) => {
7244
+ const submitted = revision.current;
7245
+ setTesting(true);
7246
+ try {
7247
+ const evaluation = await testRuleSet({ definition, input });
7248
+ if (revision.current === submitted) setResult(evaluation);
7249
+ } catch (err) {
7250
+ if (revision.current === submitted) setResult(null);
7251
+ toast.error(
7252
+ err instanceof Error ? err.message : tr("fluidgrids.rules.testFailed", "Failed to test the rule set")
7253
+ );
7254
+ } finally {
7255
+ setTesting(false);
7256
+ }
7257
+ },
7258
+ [testRuleSet, tr]
7259
+ );
7260
+ return { testing, result, invalidate, runTest };
7261
+ }
7262
+ function useRuleSetSave(initial, onInvalidSample) {
7263
+ const tr = useTr();
7264
+ const { updateRuleSet } = useRuleSetMutations();
7265
+ const editRevision = useRef(0);
7266
+ const [saved, setSaved] = useState({
7267
+ version: initial.version,
7268
+ name: initial.name
7269
+ });
7270
+ const [issues, setIssues] = useState(initial.issues);
7271
+ const [dirty, setDirty] = useState(false);
7272
+ const [saving, setSaving] = useState(false);
7273
+ const markDirty = useCallback(() => {
7274
+ editRevision.current += 1;
7275
+ setDirty(true);
7276
+ }, []);
7277
+ const save = useCallback(
7278
+ async (draft) => {
7279
+ const submitted = editRevision.current;
7280
+ if (!draft.name.trim()) {
7281
+ toast.error(
7282
+ tr(
7283
+ "fluidgrids.rules.nameRequired",
7284
+ "Give the rule set a name before saving."
7285
+ )
7286
+ );
7287
+ return;
7288
+ }
7289
+ const sample = parseJsonText(draft.inputText);
7290
+ if (!sample.ok) {
7291
+ onInvalidSample();
7292
+ toast.error(
7293
+ tr(
7294
+ "fluidgrids.rules.sampleInvalid",
7295
+ "Fix the sample input before saving: {{error}}",
7296
+ { error: sample.error }
7297
+ )
7298
+ );
7299
+ return;
7300
+ }
7301
+ setSaving(true);
7302
+ try {
7303
+ const next = await updateRuleSet(initial.id, {
7304
+ name: draft.name.trim(),
7305
+ definition: draft.definition,
7306
+ sampleInput: sample.value,
7307
+ expectedVersion: saved.version
7308
+ });
7309
+ setSaved({ version: next.version, name: next.name });
7310
+ setIssues(next.issues);
7311
+ if (editRevision.current === submitted) setDirty(false);
7312
+ toast.success(tr("fluidgrids.rules.saved", "Rule set saved"));
7313
+ } catch (err) {
7314
+ toast.error(
7315
+ err instanceof Error ? err.message : tr("fluidgrids.rules.saveFailed", "Failed to save the rule set")
7316
+ );
7317
+ } finally {
7318
+ setSaving(false);
7319
+ }
7320
+ },
7321
+ [initial.id, onInvalidSample, saved, tr, updateRuleSet]
7322
+ );
7323
+ return { saved, issues, dirty, saving, markDirty, save };
7324
+ }
7325
+ function RuleEditor({ initial }) {
7326
+ const tr = useTr();
7327
+ const navigate = useNavigate();
7328
+ const { resolvedTheme = "light" } = useTheme();
7329
+ const { screenToFlowPosition, setCenter, getNode } = useReactFlow();
7330
+ const { testing, result, invalidate, runTest } = useRuleTestRunner();
7331
+ const { tables: dataTables } = useDataTables();
7332
+ const { allowed: canEdit } = useFluidGridsPermission(
7333
+ FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE
7334
+ );
7335
+ const [seed] = useState(() => {
7336
+ const definition = readDefinition(initial.definition);
7337
+ return { definition, flow: toFlow(definition) };
7338
+ });
7339
+ const [nodes, setNodes, onNodesChange] = useNodesState(
7340
+ seed.flow.nodes
7341
+ );
7342
+ const [edges, setEdges, onEdgesChange] = useEdgesState(
7343
+ seed.flow.edges
7344
+ );
7345
+ const [startNodeId, setStartNodeId] = useState(
7346
+ seed.definition.startNodeId
7347
+ );
7348
+ const [name, setName] = useState(initial.name);
7349
+ const [inputText, setInputText] = useState(
7350
+ () => JSON.stringify(initial.sampleInput ?? {}, null, 2)
7351
+ );
7352
+ const [selectedId, setSelectedId] = useState(null);
7353
+ const [panel, setPanel] = useState("block");
7354
+ const { saved, issues, dirty, saving, markDirty, save } = useRuleSetSave(
7355
+ initial,
7356
+ () => setPanel("test")
7357
+ );
7358
+ const { effectiveStart, displayNodes, displayEdges } = useDecoratedGraph(
7359
+ nodes,
7360
+ edges,
7361
+ startNodeId,
7362
+ result
7363
+ );
7364
+ const markTestInputDirty = useCallback(() => {
7365
+ markDirty();
7366
+ invalidate();
7367
+ }, [markDirty, invalidate]);
7368
+ const onConnect = useCallback(
7369
+ (connection) => {
7370
+ if (!canConnect(
7371
+ edges,
7372
+ connection.source,
7373
+ connection.target,
7374
+ connection.sourceHandle
7375
+ )) {
7376
+ toast.error(
7377
+ tr(
7378
+ "fluidgrids.rules.connectRefused",
7379
+ "That output is already connected. Remove its connection first."
7380
+ )
7381
+ );
7382
+ return;
7383
+ }
7384
+ const branch = branchFromHandle(connection.sourceHandle);
7385
+ setEdges(
7386
+ (current) => addEdge(
7387
+ toFlowEdge({
7388
+ id: nextEdgeId(current),
7389
+ source: connection.source,
7390
+ target: connection.target,
7391
+ branch
7392
+ }),
7393
+ current
7394
+ )
7395
+ );
7396
+ markTestInputDirty();
7397
+ },
7398
+ [edges, setEdges, markTestInputDirty, tr]
7399
+ );
7400
+ const addBlock = useCallback(
7401
+ (type, position) => {
7402
+ const lowest = nodes.reduce(
7403
+ (max, node) => Math.max(max, node.position.y),
7404
+ -160
7405
+ );
7406
+ const block = createRuleNode(
7407
+ type,
7408
+ position ?? { x: 0, y: lowest + 160 },
7409
+ nodes.map((node) => node.id),
7410
+ ruleBlockTypeLabel(tr, type)
7411
+ );
7412
+ const flowNode = toFlow({ startNodeId: null, nodes: [block], edges: [] }).nodes[0];
7413
+ setNodes((current) => [...current, flowNode]);
7414
+ setSelectedId(block.id);
7415
+ setPanel("block");
7416
+ markTestInputDirty();
7417
+ },
7418
+ [nodes, setNodes, markTestInputDirty, tr]
7419
+ );
7420
+ const onDrop = useCallback(
7421
+ (event) => {
7422
+ event.preventDefault();
7423
+ const type = event.dataTransfer.getData(RULE_BLOCK_DRAG_MIME);
7424
+ if (!RULE_NODE_TYPES.includes(type)) return;
7425
+ addBlock(
7426
+ type,
7427
+ screenToFlowPosition({ x: event.clientX, y: event.clientY })
7428
+ );
7429
+ },
7430
+ [addBlock, screenToFlowPosition]
7431
+ );
7432
+ const handleNodesChange = useCallback(
7433
+ (changes) => {
7434
+ onNodesChange(changes);
7435
+ if (changes.some(
7436
+ (change) => change.type !== "select" && change.type !== "dimensions"
7437
+ )) {
7438
+ markTestInputDirty();
7439
+ }
7440
+ },
7441
+ [onNodesChange, markTestInputDirty]
7442
+ );
7443
+ const handleEdgesChange = useCallback(
7444
+ (changes) => {
7445
+ onEdgesChange(changes);
7446
+ if (changes.some((change) => change.type === "remove"))
7447
+ markTestInputDirty();
7448
+ },
7449
+ [onEdgesChange, markTestInputDirty]
7450
+ );
7451
+ const selectedNode = nodes.find((node) => node.id === selectedId) ?? null;
7452
+ const updateSelected = useCallback(
7453
+ (patch) => {
7454
+ if (!selectedId) return;
7455
+ setNodes(
7456
+ (current) => current.map(
7457
+ (node) => node.id === selectedId ? { ...node, data: { ...node.data, ...patch } } : node
7458
+ )
7459
+ );
7460
+ markTestInputDirty();
7461
+ },
7462
+ [selectedId, setNodes, markTestInputDirty]
7463
+ );
7464
+ const deleteSelected = useCallback(() => {
7465
+ if (!selectedId) return;
7466
+ setNodes((current) => current.filter((node) => node.id !== selectedId));
7467
+ setEdges(
7468
+ (current) => current.filter(
7469
+ (edge) => edge.source !== selectedId && edge.target !== selectedId
7470
+ )
7471
+ );
7472
+ if (startNodeId === selectedId) setStartNodeId(null);
7473
+ setSelectedId(null);
7474
+ markTestInputDirty();
7475
+ }, [selectedId, startNodeId, setNodes, setEdges, markTestInputDirty]);
7476
+ const focusNode = useCallback(
7477
+ (nodeId) => {
7478
+ setSelectedId(nodeId);
7479
+ const node = getNode(nodeId);
7480
+ if (node) {
7481
+ void setCenter(node.position.x + 120, node.position.y + 50, {
7482
+ zoom: 1,
7483
+ duration: 300
7484
+ });
7485
+ }
7486
+ },
7487
+ [getNode, setCenter]
7488
+ );
7489
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-[calc(100vh-4rem)] min-h-[560px] flex-col bg-background-primary", children: [
7490
+ /* @__PURE__ */ jsx(
7491
+ RuleBuilderHeader,
7492
+ {
7493
+ version: saved.version,
7494
+ name,
7495
+ dirty,
7496
+ saving,
7497
+ canEdit,
7498
+ issues,
7499
+ onBack: () => navigate("..", { relative: "path" }),
7500
+ onNameChange: (value) => {
7501
+ setName(value);
7502
+ markDirty();
7503
+ },
7504
+ onSave: () => void save({
7505
+ name,
7506
+ definition: fromFlow(nodes, edges, startNodeId),
7507
+ inputText
7508
+ })
7509
+ }
7510
+ ),
7511
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col lg:flex-row", children: [
7512
+ /* @__PURE__ */ jsx(RulePalette, { canEdit, onAdd: (type) => addBlock(type) }),
7513
+ /* @__PURE__ */ jsx(
7514
+ RuleCanvas,
7515
+ {
7516
+ nodes: displayNodes,
7517
+ edges: displayEdges,
7518
+ isEmpty: nodes.length === 0,
7519
+ canEdit,
7520
+ colorMode: resolvedTheme === "dark" ? "dark" : "light",
7521
+ onNodesChange: handleNodesChange,
7522
+ onEdgesChange: handleEdgesChange,
7523
+ onConnect,
7524
+ onDrop,
7525
+ onNodeClick: (nodeId) => {
7526
+ setSelectedId(nodeId);
7527
+ setPanel("block");
7528
+ },
7529
+ onNodesDelete: (deletedIds) => {
7530
+ if (startNodeId && deletedIds.includes(startNodeId)) {
7531
+ setStartNodeId(null);
7532
+ }
7533
+ setSelectedId(null);
7534
+ },
7535
+ onPaneClick: () => setSelectedId(null)
7536
+ }
7537
+ ),
7538
+ /* @__PURE__ */ jsx(
7539
+ RuleSidePanel,
7540
+ {
7541
+ panel,
7542
+ onPanelChange: setPanel,
7543
+ selectedNode,
7544
+ effectiveStart,
7545
+ canEdit,
7546
+ dataTables,
7547
+ onChangeSelected: updateSelected,
7548
+ onDeleteSelected: deleteSelected,
7549
+ onMakeStart: (nodeId) => {
7550
+ setStartNodeId(nodeId);
7551
+ markTestInputDirty();
7552
+ },
7553
+ issues,
7554
+ onFocusNode: focusNode,
7555
+ inputText,
7556
+ onInputTextChange: (text) => {
7557
+ setInputText(text);
7558
+ markTestInputDirty();
7559
+ },
7560
+ onRunTest: (input) => void runTest(fromFlow(nodes, edges, startNodeId), input),
7561
+ testing,
7562
+ result
7563
+ }
7564
+ )
7565
+ ] })
7566
+ ] });
7567
+ }
7568
+ function RuleBuilderLoader({ ruleSetId }) {
7569
+ const tr = useTr();
7570
+ const navigate = useNavigate();
7571
+ const { ruleSet, loading, error } = useRuleSet(ruleSetId);
7572
+ if (loading && !ruleSet) {
7573
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-[60vh] items-center justify-center text-text-secondary", children: [
7574
+ /* @__PURE__ */ jsx(Loader2, { className: "mr-2 size-5 animate-spin", "aria-hidden": "true" }),
7575
+ tr("fluidgrids.rules.loadingRuleSet", "Loading rule set\u2026")
7576
+ ] });
7577
+ }
7578
+ if (!ruleSet) {
7579
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-[60vh] flex-col items-center justify-center gap-3 text-text-secondary", children: [
7580
+ /* @__PURE__ */ jsx("p", { children: error?.message ?? tr("fluidgrids.rules.notFound", "This rule set was not found.") }),
7581
+ /* @__PURE__ */ jsxs(
7582
+ Button$1,
7583
+ {
7584
+ variant: "outline",
7585
+ onClick: () => navigate("..", { relative: "path" }),
7586
+ children: [
7587
+ /* @__PURE__ */ jsx(ArrowLeft, { className: "mr-2 size-4", "aria-hidden": "true" }),
7588
+ tr("fluidgrids.rules.backToRuleSets", "Back to rule sets")
7589
+ ]
7590
+ }
7591
+ )
7592
+ ] });
7593
+ }
7594
+ return /* @__PURE__ */ jsx(RuleEditor, { initial: ruleSet }, ruleSet.id);
7595
+ }
7596
+ function RuleBuilderPage() {
7597
+ const { ruleSetId } = useParams();
7598
+ return /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(RuleBuilderLoader, { ruleSetId: ruleSetId ?? "" }, ruleSetId) });
7599
+ }
5024
7600
 
5025
7601
  // src/graphql/client-helpers.ts
5026
7602
  function createFluidGridsClients(config2) {
@@ -5104,7 +7680,7 @@ var NODE_STATUS_CONFIG = {
5104
7680
  bgColor: "bg-bg-sunken"
5105
7681
  }
5106
7682
  };
5107
- var Textarea4 = React3.forwardRef(
7683
+ var Textarea7 = React3.forwardRef(
5108
7684
  ({ className, ...props }, ref) => {
5109
7685
  return /* @__PURE__ */ jsx(
5110
7686
  "textarea",
@@ -5119,7 +7695,7 @@ var Textarea4 = React3.forwardRef(
5119
7695
  );
5120
7696
  }
5121
7697
  );
5122
- Textarea4.displayName = "Textarea";
7698
+ Textarea7.displayName = "Textarea";
5123
7699
  function Tabs2({
5124
7700
  className,
5125
7701
  ...props
@@ -6125,4 +8701,4 @@ function createFluidGridsAgentTools(deps) {
6125
8701
  ];
6126
8702
  }
6127
8703
 
6128
- export { AiStudioPage, ApprovalInboxPage, Checkbox, DataTablesPage, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FluidGridsRoot, NODE_STATUS_CONFIG, RUN_STATUS_CONFIG, RunnersPage, ScrollArea, ScrollBar, Select3 as Select, SelectContent3 as SelectContent, SelectGroup, SelectItem3 as SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger3 as SelectTrigger, SelectValue3 as SelectValue, Separator, Switch2 as Switch, Tabs2 as Tabs, TabsContent2 as TabsContent, TabsList2 as TabsList, TabsTrigger2 as TabsTrigger, Textarea4 as Textarea, VariablesPage, WorkflowEmbedPage, createFluidGridsAgentTools, createFluidGridsClients, csvToRows, parseCsv, pickTriggerNode, rowsToCsv, triggerNodesOf, useCreateDataTable, useCreateVariable, useDataTableRows, useDataTables, useDecideToolApproval, useDeleteDataTable, useDeleteDataTableRows, useDeleteVariable, useInsertDataTableRow, usePendingToolApprovals, useRevealVariableValue, useSaveSecretsProviderConfig, useSecretsProviderConfigs, useTestSecretsProviderConnection, useUpdateDataTable, useUpdateDataTableRow, useUpdateVariable, useVariables };
8704
+ export { AiStudioPage, ApprovalInboxPage, Checkbox, DataTablesPage, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FluidGridsRoot, NODE_STATUS_CONFIG, RUN_STATUS_CONFIG, RuleBuilderPage, RuleSetsPage, RunnersPage, ScrollArea, ScrollBar, Select3 as Select, SelectContent3 as SelectContent, SelectGroup, SelectItem3 as SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger3 as SelectTrigger, SelectValue3 as SelectValue, Separator, Switch2 as Switch, Tabs2 as Tabs, TabsContent2 as TabsContent, TabsList2 as TabsList, TabsTrigger2 as TabsTrigger, Textarea7 as Textarea, VariablesPage, WorkflowEmbedPage, createFluidGridsAgentTools, createFluidGridsClients, csvToRows, parseCsv, pickTriggerNode, rowsToCsv, triggerNodesOf, useCreateDataTable, useCreateVariable, useDataTableRows, useDataTables, useDecideToolApproval, useDeleteDataTable, useDeleteDataTableRows, useDeleteVariable, useInsertDataTableRow, usePendingToolApprovals, useRevealVariableValue, useRuleSet, useRuleSetMutations, useRuleSetTemplates, useRuleSets, useSaveSecretsProviderConfig, useSecretsProviderConfigs, useTestSecretsProviderConnection, useUpdateDataTable, useUpdateDataTableRow, useUpdateVariable, useVariables };