@burdenoff/fluidgrids-fe-sdk 2026.1102.2 → 2026.1103.1
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/{WorkflowBuilder-ZIXEU7DQ.js → WorkflowBuilder-E5MFEECZ.js} +2 -2
- package/dist/{chunk-YVOHCC7J.js → chunk-4XV2DAVO.js} +1 -1
- package/dist/{chunk-MY752OLT.js → chunk-6O2B5NAK.js} +21 -4
- package/dist/{chunk-37BKVPO7.js → chunk-DQLSF6GB.js} +1 -1
- package/dist/{chunk-OZCJHPMH.js → chunk-SA474Y5U.js} +1 -1
- package/dist/index.d.ts +206 -1
- package/dist/index.js +2589 -13
- package/dist/templates.js +2 -2
- package/dist/workflow-builder.js +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@ export { Label } from './chunk-VTQVPNUQ.js';
|
|
|
2
2
|
import { useRunners, useRunnerActions, RunnerStatusPill, runnerInstallCommand } from './chunk-OR7KZZ2D.js';
|
|
3
3
|
export { RUNNER_INSTALL_SCRIPT_URL, RunOnSection, RunnerStatusPill, runnerInstallCommand, useRunnerActions, useRunners } from './chunk-OR7KZZ2D.js';
|
|
4
4
|
export { Popover, PopoverContent, PopoverTrigger } from './chunk-GTEBQQ7J.js';
|
|
5
|
-
export { WorkflowBuilder, WorkflowBuilderCore, WorkflowSearchRedirect } from './chunk-
|
|
6
|
-
export { GuidedTourStarter, WorkflowBuilder as WorkflowBuilderPage } from './chunk-
|
|
5
|
+
export { WorkflowBuilder, WorkflowBuilderCore, WorkflowSearchRedirect } from './chunk-SA474Y5U.js';
|
|
6
|
+
export { GuidedTourStarter, WorkflowBuilder as WorkflowBuilderPage } from './chunk-DQLSF6GB.js';
|
|
7
7
|
export { FirstWorkflowChecklist, FirstWorkflowChecklistView, WorkflowsList, useCreateWorkflowFirst } from './chunk-7SY2SL5Y.js';
|
|
8
8
|
import { CreateWorkflowRefusalDialog } from './chunk-JCHE4V55.js';
|
|
9
9
|
export { CreateWorkflowRefusalDialog, GUIDED_FIRST_WORKFLOW_NAV, GUIDED_FIRST_WORKFLOW_STATE, GUIDED_TOUR_STATE, firstWorkflowProgress, isGuidedTourIntent, pickGuidedTour } from './chunk-JCHE4V55.js';
|
|
@@ -24,10 +24,10 @@ import './chunk-A7QPQ6MO.js';
|
|
|
24
24
|
import './chunk-DLKGWGRN.js';
|
|
25
25
|
export { useDataSinkData, useDataSinks, useInsightsData, useLogStreamDestinationActions, useLogStreamDestinations, useRunReplayActions } from './chunk-CXNJKM4D.js';
|
|
26
26
|
import './chunk-5ERAJC2I.js';
|
|
27
|
-
import { ThemeProvider, extractTemplateGraph } from './chunk-
|
|
28
|
-
export { TemplatePreview, TemplatesPage } from './chunk-
|
|
29
|
-
import { useAiWorkflowTemplates, AI_DRAFT_MAX_PROMPT_LENGTH, FlowBuilderPage, FluidGridsMark, draftToCanvas, enrichDraftNodes } from './chunk-
|
|
30
|
-
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, RunsUsageMeter, RunsUsageMeterView, readRunsQuota, runsQuotaLevel, summarizeRunsQuota, useRunsQuota } from './chunk-
|
|
27
|
+
import { ThemeProvider, extractTemplateGraph } from './chunk-4XV2DAVO.js';
|
|
28
|
+
export { TemplatePreview, TemplatesPage } from './chunk-4XV2DAVO.js';
|
|
29
|
+
import { useAiWorkflowTemplates, AI_DRAFT_MAX_PROMPT_LENGTH, FlowBuilderPage, FluidGridsMark, draftToCanvas, enrichDraftNodes } from './chunk-6O2B5NAK.js';
|
|
30
|
+
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, RunsUsageMeter, RunsUsageMeterView, readRunsQuota, runsQuotaLevel, summarizeRunsQuota, useRunsQuota } from './chunk-6O2B5NAK.js';
|
|
31
31
|
import './chunk-GEMNVFPK.js';
|
|
32
32
|
import './chunk-B6CCE2RW.js';
|
|
33
33
|
export { Input } from './chunk-IEVSFCFS.js';
|
|
@@ -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,
|
|
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';
|
|
@@ -95,7 +97,7 @@ var WorkflowsList2 = lazy(
|
|
|
95
97
|
() => import('./WorkflowsList-NYSQEZWU.js').then((m) => ({ default: m.WorkflowsList }))
|
|
96
98
|
);
|
|
97
99
|
var WorkflowBuilder3 = lazy(
|
|
98
|
-
() => import('./WorkflowBuilder-
|
|
100
|
+
() => import('./WorkflowBuilder-E5MFEECZ.js').then((m) => ({
|
|
99
101
|
default: m.WorkflowBuilder
|
|
100
102
|
}))
|
|
101
103
|
);
|
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
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 };
|